commit 547655c326aefeb144c8454c6cd0fcb06650fb09 Author: / Date: Sun Dec 29 21:15:58 2024 +0100 zz diff --git a/GlobalShared/API/GlobalAPI.cs b/GlobalShared/API/GlobalAPI.cs new file mode 100644 index 0000000..bc7d71b --- /dev/null +++ b/GlobalShared/API/GlobalAPI.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using Global.Shared.OcTree; +using Global.Shared.OcTree.Data; +using Global.Shared.Plugin; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using VRageMath; + +namespace Global.Shared.API +{ + public static class GlobalAPI + { + private static OcTreeHandler OcTreeHandler => GlobalStatic.OcTreeHandler; + + public static bool IsAwareOfGrid(long entityId) + { + return OcTreeHandler.GridTree.Contains(entityId); + } + + public static IGridData GetGridById(long entityId) + { + IGridData grid = OcTreeHandler.GridTree.Get(entityId); + if (grid != null) return grid; + var entity = MyAPIGateway.Entities.GetEntityById(entityId); + if (!(entity is MyCubeGrid g)) + { + var text = entity != null ? $"a {entity.GetType().Name}" : "non existent"; + GlobalInstance.Log.Error($"GetGridById: Entity is not a grid, it is a {text}"); + return null; + } + + grid = OcTreeHandler.AddGrid(g); + + return grid; + } + + public static IEnumerable GetAllGrids() + { + return OcTreeHandler.GridTree.GetAll(); + } + + #region Experimental Spatial Data API + + public static IEnumerable LocateGridsExperimental(BoundingBoxD box) + { + return OcTreeHandler.GridTree.GetEnumerable(box); + } + + public static IEnumerable LocateGridsExperimental(BoundingSphereD sphere) + { + return OcTreeHandler.GridTree.GetEnumerable(sphere); + } + + public static IEnumerable LocateGridsExperimental(MyOrientedBoundingBoxD box) + { + return OcTreeHandler.GridTree.GetEnumerable(box); + } + + public static IEnumerable LocateCharactersExperimental(BoundingBoxD box) + { + return OcTreeHandler.CharacterTree.GetEnumerable(box); + } + + public static IEnumerable LocateCharactersExperimental(BoundingSphereD sphere) + { + return OcTreeHandler.CharacterTree.GetEnumerable(sphere); + } + + public static IEnumerable LocateCharactersExperimental(MyOrientedBoundingBoxD box) + { + return OcTreeHandler.CharacterTree.GetEnumerable(box); + } + + #endregion + + #region Spatial Data API + + public static void LocateGrids(List list, BoundingBoxD box, long flags = 0L) + { + OcTreeHandler.GridTree.GetExact(list, box, flags); + } + + public static void LocateGrids(List list, BoundingSphereD sphere, long flags = 0L) + { + OcTreeHandler.GridTree.GetExact(list, sphere, flags); + } + + public static void LocateGrids(List list, MyOrientedBoundingBoxD box, long flags = 0L) + { + OcTreeHandler.GridTree.GetExact(list, box, flags); + } + + public static void LocateCharacters(List list, BoundingBoxD box, long flags = 0L) + { + OcTreeHandler.CharacterTree.GetExact(list, box, flags); + } + + public static void LocateCharacters(List list, BoundingSphereD sphere, + long flags = 0L) + { + OcTreeHandler.CharacterTree.GetExact(list, sphere, flags); + } + + public static void LocateCharacters(List list, MyOrientedBoundingBoxD box, + long flags = 0L) + { + OcTreeHandler.CharacterTree.GetExact(list, box, flags); + } + + #endregion + + #region Extensions + + private static readonly List _internalGridList = new List(); + + private static readonly List + _internalCharacterList = new List(); + + private static void GetExact(this GenericOcTree ocTree, List list, BoundingSphereD sphere, + long flags = 0L) + { + _internalGridList.Clear(); + ocTree.GetExact(_internalGridList, sphere, flags); + list.AddRange(_internalGridList); + } + + private static void GetExact(this GenericOcTree ocTree, List list, BoundingBoxD box, + long flags = 0L) + { + _internalGridList.Clear(); + ocTree.GetExact(_internalGridList, box, flags); + list.AddRange(_internalGridList); + } + + private static void GetExact(this GenericOcTree ocTree, List list, + MyOrientedBoundingBoxD box, + long flags = 0L) + { + _internalGridList.Clear(); + ocTree.GetExact(_internalGridList, box, flags); + list.AddRange(_internalGridList); + } + + private static void GetExact(this GenericOcTree ocTree, + List list, BoundingSphereD sphere, + long flags = 0L) + { + _internalCharacterList.Clear(); + ocTree.GetExact(_internalCharacterList, sphere, flags); + list.AddRange(_internalCharacterList); + } + + private static void GetExact(this GenericOcTree ocTree, + List list, BoundingBoxD box, + long flags = 0L) + { + _internalCharacterList.Clear(); + ocTree.GetExact(_internalCharacterList, box, flags); + list.AddRange(_internalCharacterList); + } + + private static void GetExact(this GenericOcTree ocTree, + List list, + MyOrientedBoundingBoxD box, + long flags = 0L) + { + _internalCharacterList.Clear(); + ocTree.GetExact(_internalCharacterList, box, flags); + list.AddRange(_internalCharacterList); + } + + #endregion + } +} \ No newline at end of file diff --git a/GlobalShared/API/IBooleanFunction.cs b/GlobalShared/API/IBooleanFunction.cs new file mode 100644 index 0000000..300c9b0 --- /dev/null +++ b/GlobalShared/API/IBooleanFunction.cs @@ -0,0 +1,17 @@ +namespace Global.Shared.API +{ + public static class BooleanFunctions + { + public delegate bool BooleanFunction(bool a, bool b); + + public static BooleanFunction True = (a, b) => true; + public static BooleanFunction False = (a, b) => false; + public static BooleanFunction And = (a, b) => a && b; + public static BooleanFunction Or = (a, b) => a || b; + public static BooleanFunction Xor = (a, b) => a ^ b; + public static BooleanFunction NotA = (a, b) => b && !a; + public static BooleanFunction NotB = (a, b) => a && !b; + public static BooleanFunction NotAAndB = (a, b) => !(a && b); + public static BooleanFunction NotAOrB = (a, b) => !(a || b); + } +} \ No newline at end of file diff --git a/GlobalShared/API/IControllableEntityData.cs b/GlobalShared/API/IControllableEntityData.cs new file mode 100644 index 0000000..cc74b5f --- /dev/null +++ b/GlobalShared/API/IControllableEntityData.cs @@ -0,0 +1,9 @@ +using Sandbox.Game.Entities; + +namespace Global.Shared.API +{ + public interface IControllableEntityData + { + IMyControllableEntity Entity { get; } + } +} \ No newline at end of file diff --git a/GlobalShared/API/ICustomData.cs b/GlobalShared/API/ICustomData.cs new file mode 100644 index 0000000..b00960a --- /dev/null +++ b/GlobalShared/API/ICustomData.cs @@ -0,0 +1,9 @@ +using System; + +namespace Global.Shared.API +{ + public interface ICustomData : IDisposable + { + IGridData GridData { get; } + } +} \ No newline at end of file diff --git a/GlobalShared/API/ICustomManager.cs b/GlobalShared/API/ICustomManager.cs new file mode 100644 index 0000000..39f6341 --- /dev/null +++ b/GlobalShared/API/ICustomManager.cs @@ -0,0 +1,7 @@ +namespace Global.Shared.API +{ + public interface ICustomManager where T : ICustomData + { + T CreateData(IGridData data); + } +} \ No newline at end of file diff --git a/GlobalShared/API/IGridData.cs b/GlobalShared/API/IGridData.cs new file mode 100644 index 0000000..e2b5703 --- /dev/null +++ b/GlobalShared/API/IGridData.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Global.Shared.OcTree.Data; +using Sandbox.Game.Entities; +using Sandbox.Game.Entities.Cube; +using VRage.ObjectBuilders; +using VRageMath; + +namespace Global.Shared.API +{ + public interface IGridData : IOcTreeData + { + long Id { get; } + BoundingBoxD BoundingBox { get; } + MyCubeGrid CubeGrid { get; } + + bool IsInVoxel { get; } + + int BlockCount { get; } + + IEnumerable GetBlocks(MyObjectBuilderType type); + + IEnumerable GetAllFunctionalBlocks(); + + bool Contains(MyObjectBuilderType type); + } +} \ No newline at end of file diff --git a/GlobalShared/API/OcTreeVariables.cs b/GlobalShared/API/OcTreeVariables.cs new file mode 100644 index 0000000..b5820af --- /dev/null +++ b/GlobalShared/API/OcTreeVariables.cs @@ -0,0 +1,34 @@ +using System; + +namespace Global.Shared.API +{ + public static class GridFlag + { + public const long None = 0; + private static int _taken; + public static long StaticGrid = CreateFlag(); + public static long DynamicGrid = CreateFlag(); + public static long SmallGrid = CreateFlag(); + public static long LargeGrid = CreateFlag(); + + public static long CreateFlag() + { + if (_taken == 31) throw new Exception("Too many grid flags created. Max is 30."); + return 1 << _taken++; + } + } + + public static class PlayerFlag + { + public const long None = 0; + private static int _taken; + public static long Character = CreateFlag(); + public static long Controlled = CreateFlag(); + + public static long CreateFlag() + { + if (_taken == 31) throw new Exception("Too many player flags created. Max is 30."); + return 1 << _taken++; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Config/IPluginConfig.cs b/GlobalShared/Config/IPluginConfig.cs new file mode 100644 index 0000000..c8a705b --- /dev/null +++ b/GlobalShared/Config/IPluginConfig.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using VRageMath; + +namespace Global.Shared.Config +{ + public interface IPluginConfig : INotifyPropertyChanged + { + bool IsEnabled { get; set; } + int Size { get; set; } + Vector3I CenterPosition { get; set; } + int Capacity { get; set; } + int MaxDepth { get; set; } + } +} \ No newline at end of file diff --git a/GlobalShared/Events/BlockEvents.cs b/GlobalShared/Events/BlockEvents.cs new file mode 100644 index 0000000..9fde969 --- /dev/null +++ b/GlobalShared/Events/BlockEvents.cs @@ -0,0 +1,27 @@ +using System; +using Sandbox.Definitions; +using Sandbox.Game.Entities; +using VRageMath; + +namespace Global.Shared.Events +{ + public class BlockEvents + { + public delegate void CanPlaceBlockDelegate(MyCubeGrid grid, MyBlockOrientation? orientation, + MyCubeBlockDefinition blockDefinition, ulong placingPlayer, ref bool canPlace); + + public static Action OnPlayerControlAcquired; + public static Action OnPlayerControlReleased; + + public static Action OnBlockAdded; + public static Action OnBlockRemoved; + + public static event CanPlaceBlockDelegate CanPlaceBlockEvent; + + internal static void OnCanPlaceBlockEvent(MyCubeGrid grid, MyBlockOrientation? orientation, + MyCubeBlockDefinition def, ulong placingPlayer, ref bool canPlace) + { + CanPlaceBlockEvent?.Invoke(grid, orientation, def, placingPlayer, ref canPlace); + } + } +} \ No newline at end of file diff --git a/GlobalShared/Events/GridEvents.cs b/GlobalShared/Events/GridEvents.cs new file mode 100644 index 0000000..cc09cab --- /dev/null +++ b/GlobalShared/Events/GridEvents.cs @@ -0,0 +1,13 @@ +using System; +using Sandbox.Game.Entities; + +namespace Global.Shared.Events +{ + public class GridEvents + { + public static Action GridCreated = l => { }; + public static Action GridRemoved = l => { }; + public static Action SubGridAdded = (parent, child) => { }; + public static Action SubGridRemoved = (parent, child) => { }; + } +} \ No newline at end of file diff --git a/GlobalShared/GlobalShared.projitems b/GlobalShared/GlobalShared.projitems new file mode 100644 index 0000000..653fbcf --- /dev/null +++ b/GlobalShared/GlobalShared.projitems @@ -0,0 +1,49 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + B127E405-3C0F-48C3-9F8E-7437E8F4E106 + + + Global.Shared + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GlobalShared/GlobalShared.shproj b/GlobalShared/GlobalShared.shproj new file mode 100644 index 0000000..8df249f --- /dev/null +++ b/GlobalShared/GlobalShared.shproj @@ -0,0 +1,13 @@ + + + + {C5784FE0-CF0A-4870-9DEF-7BEA8B64C01A} + v4.8 + Shared + + + + + + + \ No newline at end of file diff --git a/GlobalShared/Logging/IPluginLogger.cs b/GlobalShared/Logging/IPluginLogger.cs new file mode 100644 index 0000000..22cff63 --- /dev/null +++ b/GlobalShared/Logging/IPluginLogger.cs @@ -0,0 +1,28 @@ +using System; + +namespace Global.Shared.Logging +{ + public interface IPluginLogger + { + bool IsTraceEnabled { get; } + bool IsDebugEnabled { get; } + bool IsInfoEnabled { get; } + bool IsWarningEnabled { get; } + bool IsErrorEnabled { get; } + bool IsCriticalEnabled { get; } + + void Trace(Exception ex, string message, params object[] data); + void Debug(Exception ex, string message, params object[] data); + void Info(Exception ex, string message, params object[] data); + void Warning(Exception ex, string message, params object[] data); + void Error(Exception ex, string message, params object[] data); + void Critical(Exception ex, string message, params object[] data); + + void Trace(string message, params object[] data); + void Debug(string message, params object[] data); + void Info(string message, params object[] data); + void Warning(string message, params object[] data); + void Error(string message, params object[] data); + void Critical(string message, params object[] data); + } +} \ No newline at end of file diff --git a/GlobalShared/Logging/LogFormatter.cs b/GlobalShared/Logging/LogFormatter.cs new file mode 100644 index 0000000..6859c1b --- /dev/null +++ b/GlobalShared/Logging/LogFormatter.cs @@ -0,0 +1,90 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; + +namespace Global.Shared.Logging +{ + public class LogFormatter + { + private const int MaxExceptionDepth = 100; + private readonly string _prefix; + private readonly ThreadLocal _stringBuilder = new ThreadLocal(); + + public LogFormatter(string prefix) + { + _prefix = prefix; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected string Format(Exception ex, string message, object[] data) + { + // Allocate a single StringBuilder object per thread + var sb = _stringBuilder.Value; + if (sb == null) + { + sb = new StringBuilder(); + _stringBuilder.Value = sb; + } + + if (message == null) + message = ""; + + sb.Append(_prefix); + + sb.Append(data == null || data.Length == 0 ? message : string.Format(message, data)); + + FormatException(sb, ex); + + var text = sb.ToString(); + sb.Clear(); + + return text; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FormatException(StringBuilder sb, Exception ex) + { + if (ex == null) + return; + + for (var i = 0; i < MaxExceptionDepth; i++) + { + sb.Append("\r\n["); + sb.Append(ex.GetType().Name); + sb.Append("] "); + sb.Append(ex.Message); + + if (ex.TargetSite != null) + { + sb.Append("\r\nMethod: "); + sb.Append(ex.TargetSite); + } + + if (ex.Data.Count > 0) + { + sb.Append("\r\nData:"); + foreach (var key in ex.Data.Keys) + { + sb.Append("\r\n"); + sb.Append(key); + sb.Append(" = "); + sb.Append(ex.Data[key]); + } + } + + sb.Append("\r\nTraceback:\r\n"); + sb.Append(ex.StackTrace); + + ex = ex.InnerException; + if (ex == null) + return; + + sb.Append("\r\nInner exception:\r\n"); + } + + sb.Append($"WARNING: Not logging more than {MaxExceptionDepth} inner exceptions"); + } + } +} \ No newline at end of file diff --git a/GlobalShared/Logging/LoggingLevel.cs b/GlobalShared/Logging/LoggingLevel.cs new file mode 100644 index 0000000..f42517f --- /dev/null +++ b/GlobalShared/Logging/LoggingLevel.cs @@ -0,0 +1,20 @@ +namespace Global.Shared.Logging +{ + public enum LoggingLevel + { + Trace, + Debug, + Info, + Warning, + Error, + Fatal + } + + public static class LevelExtensions + { + public static bool IsOfLevel(this LoggingLevel level, LoggingLevel levelToCheck) + { + return (int)level <= (int)levelToCheck; + } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/Bag.cs b/GlobalShared/OcTree/Bag.cs new file mode 100644 index 0000000..2e32820 --- /dev/null +++ b/GlobalShared/OcTree/Bag.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Global.Shared.OcTree +{ + public interface IImmutableBag : IEnumerable + { + /// Returns the element at the specified position in Bag + /// the index of the element to return + /// the element at the specified position in bag + E Get(int index); + + /// Returns the number of elements in this Bag + /// the number of elements in this bag + int Size(); + + bool IsEmpty(); + + bool Contains(E element); + } + + public class Bag : IImmutableBag + { + public E[] data; + private BagEnumerator it; + protected int size; + + public Bag() : this(64) + { + } + + public Bag(int capacity) + { + data = new E[capacity]; + } + + public E this[int i] + { + get => Get(i); + set => data[i] = value; + } + + public E Get(int index) + { + return data[index]; + } + + /// Returns the number of elements in this Bag + /// the number of elements in this bag + public int Size() + { + return size; + } + + public bool IsEmpty() + { + return size == 0; + } + + public bool Contains(E e) + { + for (var i = 0; i < size; i++) + { + var e2 = data[i]; + + if (!e.Equals(e2)) return true; + } + + return false; + } + + public E Remove(int index) + { + var e = data[index]; + data[index] = data[--size]; + data[size] = default; + return e; + } + + public E RemoveLast() + { + if (IsEmpty()) return default; + + var e = data[--size]; + data[size] = default; + return e; + } + + public bool Remove(E e) + { + for (var i = 0; i < size; i++) + { + var e2 = data[i]; + + if (!e.Equals(e2)) continue; + + data[i] = data[--size]; + data[size] = default; + return true; + } + + return false; + } + + /// Returns the number of elements this bag can hold without growing. + /// the number of elements this bag can hold without growing + public int Capacity() + { + return data.Length; + } + + public void Add(E e) + { + if (size == data.Length) Grow(data.Length + 1); + + data[size++] = e; + } + + public void Grow(int newCapacity) + { + Array.Resize(ref data, newCapacity); + } + + #region Enumerator + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerator GetEnumerator() + { + if (it == null) it = new BagEnumerator(this); + + it.Cursor = -1; + + return it; + } + + public class BagEnumerator : IEnumerator + { + private readonly Bag bag; + internal int Cursor; + + public BagEnumerator(Bag bag) + { + this.bag = bag; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + Cursor++; + return Cursor < bag.size; + } + + public void Reset() + { + Cursor = -1; + } + + object IEnumerator.Current => Current; + + public E Current => bag.data[Cursor]; + } + + #endregion + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/Data/ControllableEntityData.cs b/GlobalShared/OcTree/Data/ControllableEntityData.cs new file mode 100644 index 0000000..1e6543c --- /dev/null +++ b/GlobalShared/OcTree/Data/ControllableEntityData.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Global.Shared.API; +using Global.Shared.Plugin; +using Sandbox.Game.Entities; +using Sandbox.Game.Entities.Character; +using VRage.Game.Components; +using VRageMath; + +namespace Global.Shared.OcTree.Data +{ + public class ControllableEntityData : IControllableEntityData, IOcTreeData + { + public ControllableEntityData(IMyControllableEntity entity) + { + Entity = entity; + Id = entity.Entity.EntityId; + Recompute(); + entity.Entity.PositionComp.OnPositionChanged += PositionChanged; + } + + public IMyControllableEntity Entity { get; set; } + public long Id { get; } + public List ChildIds { get; } = new List(); + public long Flags { get; private set; } + public BoundingBoxD BoundingBox { get; private set; } + + public void Recompute() + { + Flags = PlayerFlag.None; + Flags |= Entity is MyCharacter ? PlayerFlag.Character : PlayerFlag.Controlled; + BoundingBox = Entity.Entity.PositionComp.WorldAABB; + } + + public void Update() + { + Recompute(); + } + + public void Dispose() + { + Entity.Entity.PositionComp.OnPositionChanged -= PositionChanged; + Entity = null; + BoundingBox = default; + } + + private void PositionChanged(MyPositionComponentBase obj) + { + GlobalStatic.OcTreeHandler.CharacterTree.Update(Id); + } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/Data/GridData.cs b/GlobalShared/OcTree/Data/GridData.cs new file mode 100644 index 0000000..b3f312a --- /dev/null +++ b/GlobalShared/OcTree/Data/GridData.cs @@ -0,0 +1,334 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Global.Shared.API; +using Global.Shared.Events; +using Global.Shared.Plugin; +using Sandbox.Game.Entities; +using Sandbox.Game.Entities.Cube; +using Sandbox.Game.World; +using VRage.Collections; +using VRage.Game; +using VRage.Game.ModAPI; +using VRage.Game.ObjectBuilders.Definitions.SessionComponents; +using VRage.ObjectBuilders; +using VRageMath; + +namespace Global.Shared.OcTree.Data +{ + public class GridData : IOcTreeData, IGridData + { + private static readonly Stopwatch _stopwatch = new Stopwatch(); + public ConcurrentDictionary Blocks; + public MyConcurrentList FunctionalBlocks; + public List SubGrids; + + public GridData(MyCubeGrid grid) + { + CubeGrid = grid; + Id = grid.EntityId; + Init(); + } + + public bool IsInVoxel { get; set; } + public int BlockCount { get; set; } + + public MyCubeGrid CubeGrid { get; } + + public IEnumerable GetBlocks(MyObjectBuilderType type) + { + // TODO: Improve memory allocation of this. + return Blocks.TryGetValue(type, out var list) + ? list.Cast().ToArray() + : Enumerable.Empty(); + } + + + public IEnumerable GetAllFunctionalBlocks() + { + return FunctionalBlocks ?? Enumerable.Empty(); + } + + public bool Contains(MyObjectBuilderType type) + { + return Blocks.ContainsKey(type) && Blocks[type].Count > 0; + } + + public void Update() + { + Recompute(); + } + + public long Id { get; set; } + public List ChildIds => SubGrids?.Select(x => x.EntityId).ToList(); + public long Flags { get; private set; } + public BoundingBoxD BoundingBox { get; private set; } + + public void Recompute() + { + Flags = GridFlag.None; + BoundingBox = CubeGrid.GetPhysicalGroupAABB(); + Flags |= CubeGrid.IsStatic ? GridFlag.StaticGrid : GridFlag.DynamicGrid; + Flags |= CubeGrid.GridSizeEnum == MyCubeSize.Large ? GridFlag.LargeGrid : GridFlag.SmallGrid; + BlockCount = CubeGrid.BlocksCount + (SubGrids?.Sum(x => x.BlocksCount) ?? 0); + } + + private bool Closing; + + public void Dispose() + { + GlobalInstance.Log.Debug($"Disposing of grid data for {Id} {CubeGrid.DisplayName}"); + if (GlobalInstance.Log.IsTraceEnabled) GlobalInstance.Log.Trace(new StackTrace(true).ToString()); + CubeGrid.OnStaticChanged -= OnStaticChanged; + CubeGrid.OnConnectionChanged -= OnConnectionChanged; + CubeGrid.OnGridMerge -= OnGridMerged; + CubeGrid.OnGridSplit -= OnGridSplit; + CubeGrid.OnFatBlockAdded -= AddBlock; + CubeGrid.OnFatBlockRemoved -= OnFatBlockRemoved; + SubGrids?.ForEach(x => + { + x.OnStaticChanged -= OnStaticChanged; + x.OnConnectionChanged -= OnConnectionChanged; + x.OnGridMerge -= OnGridMerged; + x.OnGridSplit -= OnGridSplit; + x.OnFatBlockAdded -= AddBlock; + x.OnFatBlockRemoved -= OnFatBlockRemoved; + }); + Closing = true; + GlobalInstance.Run(() => + { + Blocks.Clear(); + + FunctionalBlocks.Clear(); + SubGrids.Clear(); + }); + } + + private void OnStaticChanged(MyCubeGrid arg1, bool arg2) + { + RecheckVoxelStatus(); + } + + public void Init() + { + Blocks = new ConcurrentDictionary(); + FunctionalBlocks = new MyConcurrentList(); + RecalculateSubgrids(); + Recompute(); + CubeGrid.OnStaticChanged += OnStaticChanged; + CubeGrid.OnConnectionChanged += OnConnectionChanged; + CubeGrid.OnGridMerge += OnGridMerged; + CubeGrid.OnGridSplit += OnGridSplit; + CubeGrid.OnFatBlockAdded += AddBlock; + CubeGrid.OnFatBlockRemoved += OnFatBlockRemoved; + + if (GlobalInstance.Log.IsDebugEnabled) + { + _stopwatch.Reset(); + _stopwatch.Start(); + } + + ScanGridBlocks(CubeGrid); + if (SubGrids.Count > 0) + SubGrids.ForEach(ScanGridBlocks); + RecheckVoxelStatus(); + + if (GlobalInstance.Log.IsDebugEnabled) + GlobalInstance.Log.Debug( + $"Data Init took {_stopwatch.ElapsedMilliseconds}ms for {CubeGrid.DisplayName} with {CubeGrid.BlocksCount + SubGrids.Sum(e => e.BlocksCount)} blocks and {SubGrids.Count} subgrids"); + } + + private void OnFatBlockRemoved(MyCubeBlock block) + { + if (Closing) return; + if (Blocks.TryGetValue(block.BlockDefinition.Id.TypeId, out var bag)) bag.Remove(block); + + GlobalInstance.Run(() => + { + if (block is IMyControllableEntity entity) + { + entity.ControllerInfo.ControlAcquired -= OnControlAcquired; + entity.ControllerInfo.ControlReleased -= OnControlReleased; + OnControlReleased(entity.ControllerInfo.Controller); + } + + BlockEvents.OnBlockRemoved?.Invoke(block); + }); + } + + public void RecheckVoxelStatus() + { + if (Closing) return; + var settings = new MyGridPlacementSettings + { + VoxelPlacement = new VoxelPlacementSettings + { + PlacementMode = VoxelPlacementMode.Volumetric + } + }; + IsInVoxel = false; + if (!CheckGridVoxelStatic(CubeGrid, settings)) + if (SubGrids.Count > 0 && SubGrids.All(e => !CheckGridVoxelStatic(e, settings))) + return; + IsInVoxel = true; + } + + public bool CheckGridVoxelStatic(MyCubeGrid grid, MyGridPlacementSettings settings) + { + if (!MyCubeGrid.IsAabbInsideVoxel(grid.WorldMatrix, grid.PositionComp.LocalAABB, settings)) return false; + var worldAabb = grid.PositionComp.WorldAABB; + return MyGamePruningStructure.AnyVoxelMapInBox(ref worldAabb) && + grid.GetBlocks().Any(block => MyCubeGrid.IsInVoxels(block, isVolumetric: false)); + } + + private bool RecalculateSubgrids() + { + var previous = SubGrids; + SubGrids = CubeGrid.GetConnectedGrids(GridLinkTypeEnum.Mechanical); + SubGrids.Remove(CubeGrid); + if (previous == null) + { + SubGrids.ForEach(OnSubGridAdded); + return SubGrids.Any(); + } + + var changed = false; + var newSubGrids = SubGrids.Except(previous).ToList(); + if (newSubGrids.Count > 0) + { + changed = true; + newSubGrids.ForEach(OnSubGridAdded); + } + + var removedSubGrids = previous.Except(SubGrids).ToList(); + if (removedSubGrids.Count > 0) + { + changed = true; + removedSubGrids.ForEach(OnSubGridRemoved); + } + + if (changed) GlobalStatic.OcTreeHandler.GridTree.Update(Id); + + return changed; + } + + private void OnGridSplit(MyCubeGrid arg1, MyCubeGrid arg2) + { + GlobalInstance.Log.Debug( + $"Grid split Invoke: {arg1.DisplayName} and {arg2.DisplayName} {Thread.CurrentThread.Name}"); + GlobalInstance.Run(() => + { + GlobalInstance.Log.Debug( + $"Grid split Run: {arg1.DisplayName} and {arg2.DisplayName} {Thread.CurrentThread.Name}"); + GlobalStatic.OcTreeHandler.GridTree.Remove(arg1.EntityId); + GlobalStatic.OcTreeHandler.GridTree.Remove(arg2.EntityId); + GlobalStatic.OcTreeHandler.AddGrid(arg1); + GlobalStatic.OcTreeHandler.AddGrid(arg2); + }); + } + + private void OnGridMerged(MyCubeGrid arg1, MyCubeGrid arg2) + { + //GlobalInstance.Log.Info($"Merging {arg1.DisplayName} and {arg2.DisplayName}"); + RecalculateSubgrids(); + Recompute(); + Blocks.Clear(); + + FunctionalBlocks.Clear(); + ScanGridBlocks(CubeGrid); + if (SubGrids.Count > 0) + SubGrids.ForEach(ScanGridBlocks); + } + + private void OnConnectionChanged(MyCubeGrid arg1, GridLinkTypeEnum arg2) + { + if (Closing) return; + if (arg2 != GridLinkTypeEnum.Mechanical) return; + + RecalculateSubgrids(); + Recompute(); + Blocks.Clear(); + FunctionalBlocks.Clear(); + ScanGridBlocks(CubeGrid); + if (SubGrids.Count > 0) + SubGrids.ForEach(ScanGridBlocks); + } + + private void ScanGridBlocks(MyCubeGrid obj) + { + Parallel.ForEach(obj.GetFatBlocks(), AddBlock); + } + + private void OnSubGridAdded(MyCubeGrid grid) + { + GlobalStatic.OcTreeHandler.RemoveGrid(grid); + grid.OnFatBlockAdded += AddBlock; + grid.OnFatBlockRemoved += OnFatBlockRemoved; + grid.OnStaticChanged += OnStaticChanged; + grid.OnConnectionChanged += OnConnectionChanged; + grid.OnGridMerge += OnGridMerged; + grid.OnGridSplit += OnGridSplit; + GridEvents.SubGridAdded?.Invoke(CubeGrid, grid); + RecheckVoxelStatus(); + } + + private void OnSubGridRemoved(MyCubeGrid grid) + { + grid.OnFatBlockAdded -= AddBlock; + grid.OnFatBlockRemoved -= OnFatBlockRemoved; + grid.OnStaticChanged -= OnStaticChanged; + grid.OnConnectionChanged -= OnConnectionChanged; + grid.OnGridMerge -= OnGridMerged; + grid.OnGridSplit -= OnGridSplit; + GridEvents.SubGridRemoved?.Invoke(CubeGrid, grid); + RecheckVoxelStatus(); + } + + private void AddBlock(MyCubeBlock block) + { + if (Closing) return; + Blocks.AddOrUpdate(block.BlockDefinition.Id.TypeId, + _ => ArrayList.Synchronized(new ArrayList { block }), + (_, list) => + { + list.Add(block); + + return list; + }); + + GlobalInstance.Run(() => + { + if (block is IMyControllableEntity entity) + { + entity.ControllerInfo.ControlAcquired += OnControlAcquired; + entity.ControllerInfo.ControlReleased += OnControlReleased; + if (entity.ControllerInfo.IsLocallyControlled() || entity.ControllerInfo.IsRemotelyControlled()) + OnControlAcquired(entity.ControllerInfo.Controller); + } + + if (block is MyFunctionalBlock functionalBlock) FunctionalBlocks.Add(functionalBlock); + + BlockEvents.OnBlockAdded?.Invoke(block); + }); + } + + private void OnControlAcquired(MyEntityController obj) + { + BlockEvents.OnPlayerControlAcquired?.Invoke(obj.ControlledEntity); + } + + private void OnControlReleased(MyEntityController obj) + { + if (obj != null) BlockEvents.OnPlayerControlReleased?.Invoke(obj.ControlledEntity); + } + + public void RaiseAttachedEntityChanged() + { + OnConnectionChanged(CubeGrid, GridLinkTypeEnum.Mechanical); + } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/Data/IOcTreeData.cs b/GlobalShared/OcTree/Data/IOcTreeData.cs new file mode 100644 index 0000000..19ffc74 --- /dev/null +++ b/GlobalShared/OcTree/Data/IOcTreeData.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using VRageMath; + +namespace Global.Shared.OcTree.Data +{ + public interface IOcTreeData : IDisposable + { + long Id { get; } + List ChildIds { get; } + long Flags { get; } + BoundingBoxD BoundingBox { get; } + + void Update(); + void Recompute(); + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/GenericOcTree.cs b/GlobalShared/OcTree/GenericOcTree.cs new file mode 100644 index 0000000..a27d7ed --- /dev/null +++ b/GlobalShared/OcTree/GenericOcTree.cs @@ -0,0 +1,986 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Global.Shared.OcTree.Data; +using Global.Shared.Plugin; +using Global.Shared.Util; +using VRageMath; + +namespace Global.Shared.OcTree +{ + public class GenericOcTree : IPoolable where T : IOcTreeData + { + private readonly GenericContainer _bounds; + + private readonly List> _containersToAdd = new List>(); + + private readonly Pool> _cPool = + new Pool>(() => new GenericContainer()); + + private readonly GenericOcTree[] _nodes = new GenericOcTree[8]; + + private readonly Pool> _otPool = new Pool>(() => new GenericOcTree()); + + private int _capacity; + private Bag> _containers; + + private Dictionary> _idToContainer; + private int _maxDepth; + + private GenericOcTree _parent; + + private int treeDepth; + + private GenericOcTree() : this(0, 0, 0, 0, 0, 0, 0, 0) + { + } + + public GenericOcTree(float x, float y, float z, float width, float height, float depth, int capacity, + int maxDepth) + { + _bounds = new GenericContainer().Set(x, y, z, width, height, depth); + _capacity = capacity; + _maxDepth = maxDepth; + _containers = new Bag>(capacity); + _idToContainer = new Dictionary>(); + } + + public T this[long entityId] => Get(entityId); + + public void Reset() + { + for (var i = _containers.Size() - 1; i >= 0; i--) _cPool.Release(_containers.Remove(i)); + for (var i = 0; i < _nodes.Length; i++) + if (_nodes[i] != null) + { + _otPool.Release(_nodes[i]); + _nodes[i] = null; + } + } + + private GenericOcTree Init(int currentTreeDepth, float x, float y, float z, float width, float height, + float depth, + GenericOcTree parent) + { + treeDepth = currentTreeDepth; + _parent = parent; + _capacity = parent?._capacity ?? 1; + _maxDepth = parent?._maxDepth ?? 6; + _bounds.Set(x, y, z, width, height, depth); + _idToContainer = parent?._idToContainer ?? new Dictionary>(); + _containers = new Bag>(_capacity); + + return this; + } + + private int IndexOf(float x, float y, float z, float width, float height, float depth) + { + var midX = _bounds.X + _bounds.Width / 2; + var midY = _bounds.Y + _bounds.Height / 2; + var midZ = _bounds.Z + _bounds.Depth / 2; + + var res = 0; + if (x > midX) res |= E; + else if (x < midX && x + width < midX) res |= W; + else return OUTSIDE; + + if (y > midY) res |= U; + else if (y < midY && y + height < midY) res |= D; + else return OUTSIDE; + + if (z > midZ) res |= S; + else if (z < midZ && z + depth < midZ) res |= N; + else return OUTSIDE; + + return res; + } + + private bool ShouldMerge() + { + // If has no children, has nothing to merge + if (_nodes[0] == null) return false; + // If has any children with children, cannot merge + if (_nodes.Any(n => n._nodes[0] != null)) return false; + // If children combined contain less than capacity, can merge + return _containers.Size() + _nodes.Sum(n => n._containers.Size()) <= _capacity; + } + + private bool ShouldSplit() + { + var halfWidth = _bounds.Width / 2; + var halfHeight = _bounds.Height / 2; + var halfDepth = _bounds.Depth / 2; + var temporaryBounds = new GenericContainer(); + temporaryBounds.Set(_bounds.X, _bounds.Y, _bounds.Z, halfWidth, halfHeight, halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X + halfWidth, _bounds.Y, _bounds.Z, halfWidth, halfHeight, halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X, _bounds.Y + halfHeight, _bounds.Z, halfWidth, halfHeight, halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X + halfWidth, _bounds.Y + halfHeight, _bounds.Z, halfWidth, halfHeight, + halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X, _bounds.Y, _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X + halfWidth, _bounds.Y, _bounds.Z + halfDepth, halfWidth, halfHeight, + halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X, _bounds.Y + halfHeight, _bounds.Z + halfDepth, halfWidth, halfHeight, + halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + temporaryBounds.Set(_bounds.X + halfWidth, _bounds.Y + halfHeight, _bounds.Z + halfDepth, halfWidth, + halfHeight, halfDepth); + if (_containers.All(e => temporaryBounds.Contains(e))) return false; + return true; + } + + private void Split() + { + // if (!ShouldSplit()) return; TODO: See if necessary + + var halfWidth = _bounds.Width / 2; + var halfHeight = _bounds.Height / 2; + var halfDepth = _bounds.Depth / 2; + + _nodes[DNW] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y, _bounds.Z, halfWidth, halfHeight, + halfDepth, this); + _nodes[DNE] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y, _bounds.Z, halfWidth, + halfHeight, halfDepth, this); + _nodes[DSW] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y, _bounds.Z + halfDepth, halfWidth, + halfHeight, halfDepth, this); + _nodes[DSE] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y, + _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth, this); + + _nodes[UNW] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y + halfHeight, _bounds.Z, + halfWidth, halfHeight, halfDepth, this); + _nodes[UNE] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y + halfHeight, + _bounds.Z, halfWidth, halfHeight, halfDepth, this); + _nodes[USW] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y + halfHeight, + _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth, this); + _nodes[USE] = _otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y + halfHeight, + _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth, this); + } + + private void HandleMerge() + { + if (!ShouldMerge()) return; + + for (var index = _nodes.Length - 1; index >= 0; index--) + { + var ocTree = _nodes[index]; + for (var i = ocTree._containers.Size() - 1; i >= 0; i--) + { + var ocTreeContainer = ocTree._containers[i]; + _containersToAdd.Add(ocTreeContainer); + ocTree._containers.Remove(i); + } + } + + for (var index = _nodes.Length - 1; index >= 0; index--) + { + var ocTree = _nodes[index]; + _nodes[index] = null; + _otPool.Release(ocTree); + ocTree.Reset(); + } + + foreach (var container in _containersToAdd) Insert(container); + + _containersToAdd.Clear(); + } + + public IEnumerable GetAll() + { + if (_nodes[0] != null) + foreach (var data in _nodes.SelectMany(e => e.GetAll())) + yield return data; + foreach (var genericContainer in _containers) + if (genericContainer.Value != null) + yield return genericContainer.Value; + } + + #region Node Bitwise + + private const int OUTSIDE = -1; + private const int D = 0b000; + private const int U = 0b100; + private const int S = 0b000; + private const int N = 0b010; + private const int W = 0b000; + private const int E = 0b001; + private const int DSW = D | S | W; // 0 + private const int DSE = D | S | E; // 1 + private const int DNW = D | N | W; // 2 + private const int DNE = D | N | E; // 3 + private const int USW = U | S | W; // 4 + private const int USE = U | S | E; // 5 + private const int UNW = U | N | W; // 6 + private const int UNE = U | N | E; // 7 + + #endregion + + #region Modifiers + + public void Update(long entityId) + { + if (!Contains(entityId)) return; + var c = _idToContainer[entityId]; + try + { + var (removedChildren, addedChildren) = c.Update(); + if (c.EntityId != entityId) + { + var old = entityId; + entityId = c.EntityId; + // already contains new id.. just give up + if (Contains(entityId)) return; + // Entity id changed, update the octree + _idToContainer.Remove(old); + _idToContainer.Add(entityId, c); + } + + // Attempting to update subgrid + if (removedChildren != null) + foreach (var removedChild in removedChildren) + _idToContainer.Remove(removedChild); + if (addedChildren != null) + foreach (var addedChild in addedChildren) + _idToContainer.Add(addedChild, c); + + var parentTree = c.Parent; + if (parentTree._bounds.Contains(c)) return; + + parentTree._containers.Remove(c); + while (parentTree._parent != null && !parentTree._bounds.Contains(c)) parentTree = parentTree._parent; + + parentTree.Insert(c); + } + catch (Exception e) + { + GlobalInstance.Log.Critical(e, "Failed to update octree"); + } + } + + public void Insert(T value) + { + Insert(_cPool.Obtain().Set(value)); + } + + private void Insert(GenericContainer c) + { + if (_nodes[0] != null) + { + var index = IndexOf(c.X, c.Y, c.Z, c.Width, c.Height, c.Depth); + if (index != OUTSIDE) + { + _nodes[index].Insert(c); + return; + } + } + + c.Parent = this; + _idToContainer[c.EntityId] = c; + if (c.ChildIds != null) + foreach (var childId in c.ChildIds) + _idToContainer[childId] = c; + _containers.Add(c); + + // If under capacity or at/over max depth, we're done. + if (_containers.Size() <= _capacity || treeDepth >= _maxDepth) return; + + if (_nodes[0] == null) Split(); + if (_nodes[0] == null) return; + + var items = _containers.data; + for (var i = _containers.Size() - 1; i >= 0; i--) + { + var next = items[i]; + if (next == null) continue; + var index = IndexOf(next.X, next.Y, next.Z, next.Width, next.Height, next.Depth); + if (index == OUTSIDE) continue; + _nodes[index].Insert(next); + _containers.Remove(i); + } + } + + public void Remove(long entityId) + { + if (!_idToContainer.ContainsKey(entityId)) return; + + var container = _idToContainer[entityId]; + var parent = container.Parent; + parent?._containers.Remove(container); + _idToContainer.Remove(entityId); + if (container.ChildIds != null) + foreach (var childId in container.ChildIds) + _idToContainer.Remove(childId); + container.Value?.Dispose(); + container.Value = default; + _cPool.Release(container); + parent?._parent?.HandleMerge(); + } + + #endregion + + #region Getters + + public bool Contains(long entityId) + { + return _idToContainer.ContainsKey(entityId); + } + + public T Get(long entityId) + { + return !Contains(entityId) ? default : _idToContainer[entityId].Value; + } + + /// + /// Returns entityIds of entities that are inside the OcTrees that contain given point. + /// + /// List to fill + /// X position + /// Y position + /// Z position + public List Get(List entityIds, float x, float y, float z) + { + if (!_bounds.Contains(x, y, z)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, 0f, 0f, 0f); + if (index != OUTSIDE) _nodes[index].Get(entityIds, x, y, z); + } + + entityIds.AddRange(_containers.Select(container => container.EntityId)); + + return entityIds; + } + + public List Get(List entityIds, float x, float y, float z, long flags) + { + if (flags == 0L) return Get(entityIds, x, y, z); + if (!_bounds.Contains(x, y, z)) return entityIds; + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, 0f, 0f, 0f); + if (index != OUTSIDE) _nodes[index].Get(entityIds, x, y, z, flags); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) + select container.EntityId + ); + + return entityIds; + } + + public List Get(List entityIds, BoundingSphereD sphereD) + { + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].Get(entityIds, sphereD); + else + foreach (var ocTree in _nodes) + ocTree.Get(entityIds, sphereD); + } + + entityIds.AddRange(_containers.Select(container => container.EntityId)); + + return entityIds; + } + + public List Get(List entityIds, long flags, BoundingSphereD sphereD) + { + if (flags == 0L) return Get(entityIds, sphereD); + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].Get(entityIds, flags, sphereD); + else + foreach (var ocTree in _nodes) + ocTree.Get(entityIds, flags, sphereD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) + select container.EntityId); + + return entityIds; + } + + //TODO: Make exact check sphere rather than box + public List GetExact(List entityIds, BoundingSphereD sphereD) + { + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(entityIds, sphereD); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(entityIds, sphereD); + } + + entityIds.AddRange(from container in _containers + where container.Overlaps(x, y, z, width, height, depth) && sphereD.Intersects( + new BoundingBoxD(new Vector3D(container.X, container.Y, container.Z), + new Vector3D( + container.X + container.Width, + container.Y + container.Height, + container.Z + container.Depth + ))) + select container.EntityId); + + return entityIds; + } + + public List GetExact(List entityIds, long flags, BoundingSphereD sphereD) + { + if (flags == 0L) return GetExact(entityIds, sphereD); + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(entityIds, flags, sphereD); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(entityIds, flags, sphereD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) && + container.Overlaps(x, y, z, width, height, depth) && + sphereD.Intersects( + new BoundingBoxD(new Vector3D(container.X, container.Y, container.Z), + new Vector3D( + container.X + container.Width, + container.Y + container.Height, + container.Z + container.Depth + ))) + select container.EntityId); + + return entityIds; + } + + public List Get(List entityIds, BoundingBoxD boxD) + { + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].Get(entityIds, boxD); + else + foreach (var ocTree in _nodes) + ocTree.Get(entityIds, boxD); + } + + entityIds.AddRange(_containers.Select(container => container.EntityId)); + + return entityIds; + } + + public List Get(List entityIds, long flags, BoundingBoxD boxD) + { + if (flags == 0L) return Get(entityIds, boxD); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].Get(entityIds, flags, boxD); + else + foreach (var ocTree in _nodes) + ocTree.Get(entityIds, flags, boxD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) && container.Overlaps(x, y, z, width, height, depth) + select container.EntityId); + + return entityIds; + } + + public List GetExact(List entityIds, MyOrientedBoundingBoxD boxBoundingBox) + { + var boxD = boxBoundingBox.GetAABB(); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(entityIds, boxD); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(entityIds, boxD); + } + + foreach (var container in _containers) + { + if (!container.Overlaps(x, y, z, width, height, depth)) continue; + + var containerBox = container.BoundingBox; + if (boxBoundingBox.Intersects(ref containerBox)) entityIds.Add(container.EntityId); + } + + return entityIds; + } + + public List GetExact(List entityIds, long flags, MyOrientedBoundingBoxD boxBoundingBox) + { + if (flags == 0L) return GetExact(entityIds, boxBoundingBox); + var boxD = boxBoundingBox.GetAABB(); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(entityIds, boxD); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(entityIds, boxD); + } + + foreach (var container in _containers) + { + if (!container.MatchesFlags(flags) || + !container.Overlaps(x, y, z, width, height, depth)) continue; + + var containerBox = container.BoundingBox; + if (boxBoundingBox.Intersects(ref containerBox)) entityIds.Add(container.EntityId); + } + + return entityIds; + } + + public List GetExact(List entityIds, BoundingBoxD boxD) + { + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(entityIds, boxD); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(entityIds, boxD); + } + + entityIds.AddRange(from container in _containers + where container.Overlaps(x, y, z, width, height, depth) + select container.EntityId); + + return entityIds; + } + + public List GetExact(List entityIds, long flags, BoundingBoxD boxD) + { + if (flags == 0L) return GetExact(entityIds, boxD); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(entityIds, flags, boxD); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(entityIds, flags, boxD); + } + + entityIds.AddRange(from GenericContainer in _containers + where GenericContainer.MatchesFlags(flags) && GenericContainer.Overlaps(x, y, z, width, height, depth) + select GenericContainer.EntityId); + + return entityIds; + } + + + public void GetExact(List outList, BoundingSphereD sphereD, long flags = 0L) + { + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(outList, sphereD, flags); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(outList, sphereD, flags); + } + + if (_containers.Any()) + outList.AddRange(from container in _containers + where container.MatchesFlags(flags) && + container.Overlaps(x, y, z, width, height, depth) && + sphereD.Intersects(container.BoundingBox) + select container.Value); + } + + public void GetExact(List outList, BoundingBoxD box, long flags = 0L) + { + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(outList, box, flags); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(outList, box, flags); + } + + if (_containers.Any()) + outList.AddRange(from container in _containers + where container.MatchesFlags(flags) && + container.Overlaps(x, y, z, width, height, depth) && + box.Intersects(container.BoundingBox) + select container.Value); + } + + public void GetExact(List outList, MyOrientedBoundingBoxD boxD, long flags = 0L) + { + var box = boxD.GetAABB(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) _nodes[index].GetExact(outList, box, flags); + else + foreach (var ocTree in _nodes) + ocTree.GetExact(outList, box, flags); + } + + if (_containers.Any()) + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var container in _containers) + { + if (!container.MatchesFlags(flags) || !container.Overlaps(x, y, z, width, height, depth)) continue; + var containerBox = container.BoundingBox; + if (boxD.Intersects(ref containerBox)) outList.Add(container.Value); + } + } + + + public IEnumerable GetEnumerable(BoundingSphereD sphereD, long flags = 0L) + { + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) yield break; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) + foreach (var data in _nodes[index].GetEnumerable(sphereD, flags)) + yield return data; + else + foreach (var data in _nodes.SelectMany(e => e.GetEnumerable(sphereD, flags))) + yield return data; + } + + if (!_containers.Any()) yield break; + + foreach (var c in _containers) + if (c.MatchesFlags(flags) && c.Overlaps(x, y, z, width, height, depth) && + sphereD.Intersects(c.BoundingBox)) + yield return c.Value; + } + + + public IEnumerable GetEnumerable(BoundingBoxD box, long flags = 0L) + { + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) yield break; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) + foreach (var data in _nodes[index].GetEnumerable(box, flags)) + yield return data; + else + foreach (var data in _nodes.SelectMany(e => e.GetEnumerable(box, flags))) + yield return data; + } + + if (!_containers.Any()) yield break; + + foreach (var c in _containers) + if (c.MatchesFlags(flags) && c.Overlaps(x, y, z, width, height, depth) && box.Intersects(c.BoundingBox)) + yield return c.Value; + } + + + public IEnumerable GetEnumerable(MyOrientedBoundingBoxD boxD, long flags = 0L) + { + var box = boxD.GetAABB(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) yield break; + + if (_nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) + foreach (var data in _nodes[index].GetEnumerable(boxD, flags)) + yield return data; + else + foreach (var data in _nodes.SelectMany(e => e.GetEnumerable(boxD, flags))) + yield return data; + } + + if (!_containers.Any()) yield break; + + foreach (var c in _containers) + { + var cBox = c.BoundingBox; + if (c.MatchesFlags(flags) && c.Overlaps(x, y, z, width, height, depth) && boxD.Intersects(ref cBox)) + yield return c.Value; + } + } + + #endregion + } + + public class GenericContainer : IPoolable, IContainer where T : IOcTreeData + { + private static readonly BoundingBoxD ReusableBox = new BoundingBoxD(); + public List ChildIds; + public long EntityId; + + public long Flags; + public GenericOcTree Parent; + + public T Value; + + public BoundingBoxD BoundingBox => ReusableBox.SetToContainer(this); + public float X { get; private set; } + public float Y { get; private set; } + public float Z { get; private set; } + public float Width { get; private set; } + public float Height { get; private set; } + public float Depth { get; private set; } + + public void Reset() + { + ChildIds = null; + EntityId = -1; + Flags = 0; + X = 0; + Y = 0; + Z = 0; + Width = 0; + Height = 0; + Depth = 0; + Value = default; + } + + public GenericContainer Set(T value) + { + Value = value; + ChildIds = value.ChildIds; + EntityId = value.Id; + Flags = value.Flags; + var box = value.BoundingBox; + X = (float)box.Min.X; + Y = (float)box.Min.Y; + Z = (float)box.Min.Z; + Width = (float)box.Max.X - X; + Height = (float)box.Max.Y - Y; + Depth = (float)box.Max.Z - Z; + return this; + } + + public KeyValuePair, IEnumerable> Update() + { + if (Value == null) return new KeyValuePair, IEnumerable>(null, null); + Value.Update(); + var previousChildIds = ChildIds; + ChildIds = Value.ChildIds; + Flags = Value.Flags; + var box = Value.BoundingBox; + X = (float)box.Min.X; + Y = (float)box.Min.Y; + Z = (float)box.Min.Z; + Width = (float)box.Max.X - X; + Height = (float)box.Max.Y - Y; + Depth = (float)box.Max.Z - Z; + return new KeyValuePair, IEnumerable>( + previousChildIds.Where(e => !ChildIds.Contains(e)), + ChildIds.Where(e => !previousChildIds.Contains(e))); + } + + public GenericContainer Set(float x, float y, float z, float width, float height, float depth) + { + X = x; + Y = y; + Z = z; + Width = width; + Height = height; + Depth = depth; + return this; + } + + public bool MatchesFlags(long flags) + { + return flags == 0 || (Flags & flags) > 0; + } + + public bool Contains(float x, float y, float z) + { + return X <= x && x <= X + Width && + Y <= y && y <= Y + Height && + Z <= z && z <= Z + Depth; + } + + public bool Contains(float x, float y, float z, float width, float height, float depth) + { + var xMax = x + width; + var yMax = x + height; + var zMax = x + depth; + + return x > X && x < X + Width && xMax > X && xMax < X + Width && + y > Y && y < Y + Height && yMax > Y && yMax < Y + Height && + z > Z && z < Z + Depth && zMax > Z && zMax < Z + Depth; + } + + public bool Overlaps(float x, float y, float z, float width, float height, float depth) + { + return !(X > x + width || x > X + Width || Y > y + height || y > Y + Height || Z > z + depth || + z > Z + Depth); + } + + public bool IntersectsRay(Vector3 start, Vector3 end) + { + var lineD = new LineD(start, end); + var box = BoundingBox; + return box.Intersects(ref lineD); + } + + public bool Contains(GenericContainer c) + { + return Contains(c.X, c.Y, c.Z, c.Width, c.Height, c.Depth); + } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/IContainer.cs b/GlobalShared/OcTree/IContainer.cs new file mode 100644 index 0000000..6fec185 --- /dev/null +++ b/GlobalShared/OcTree/IContainer.cs @@ -0,0 +1,12 @@ +namespace Global.Shared.OcTree +{ + public interface IContainer + { + float X { get; } + float Y { get; } + float Z { get; } + float Width { get; } + float Height { get; } + float Depth { get; } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/OcTree.cs b/GlobalShared/OcTree/OcTree.cs new file mode 100644 index 0000000..0c54f45 --- /dev/null +++ b/GlobalShared/OcTree/OcTree.cs @@ -0,0 +1,707 @@ +using System.Collections.Generic; +using System.Linq; +using VRageMath; + +namespace Global.Shared.OcTree +{ + public class OcTree : IPoolable + { + private static int TotalIds; + private static readonly Pool cPool = new Pool(() => new Container()); + private static readonly Pool otPool = new Pool(() => new OcTree()); + + private readonly Container _bounds; + + private readonly List _containersToAdd = new List(); + private readonly OcTree[] nodes = new OcTree[8]; + private Bag _containers; + + private int capacity; + + private Dictionary idToContainer; + private int maxDepth; + + private OcTree parent; + + private BoundingBoxD reusableBox = new BoundingBoxD(); + private int treeDepth; + + private OcTree() : this(0, 0, 0, 0, 0, 0, 0, 0) + { + } + + public OcTree(float x, float y, float z, float width, float height, float depth, int capacity, + int maxDepth) + { + _bounds = new Container().Set(x, y, z, width, height, depth); + this.capacity = capacity; + this.maxDepth = maxDepth; + _containers = new Bag(capacity); + idToContainer = new Dictionary(); + } + + public void Reset() + { + for (var i = _containers.Size() - 1; i >= 0; i--) cPool.Release(_containers.Remove(i)); + for (var i = 0; i < nodes.Length; i++) + if (nodes[i] != null) + { + otPool.Release(nodes[i]); + nodes[i] = null; + } + } + + private OcTree Init(int currentTreeDepth, float x, float y, float z, float width, float height, float depth, + OcTree parent) + { + treeDepth = currentTreeDepth; + this.parent = parent; + capacity = parent?.capacity ?? 1; + maxDepth = parent?.maxDepth ?? 6; + _bounds.Set(x, y, z, width, height, depth); + idToContainer = parent?.idToContainer ?? new Dictionary(); + _containers = new Bag(capacity); + + return this; + } + + private int IndexOf(float x, float y, float z, float width, float height, float depth) + { + var midX = _bounds.X + _bounds.Width / 2; + var midY = _bounds.Y + _bounds.Height / 2; + var midZ = _bounds.Z + _bounds.Depth / 2; + + var res = 0; + if (x > midX) res |= E; + else if (x < midX && x + width < midX) res |= W; + else return OUTSIDE; + + if (y > midY) res |= U; + else if (y < midY && y + height < midY) res |= D; + else return OUTSIDE; + + if (z > midZ) res |= S; + else if (z < midZ && z + depth < midZ) res |= N; + else return OUTSIDE; + + return res; + } + + private bool ShouldMerge() + { + // If has no children, has nothing to merge + if (nodes[0] == null) return false; + // If has any children with children, cannot merge + if (nodes.Any(n => n.nodes[0] != null)) return false; + // If children combined contain less than capacity, can merge + return _containers.Size() + nodes.Sum(n => n._containers.Size()) <= capacity; + } + + private void Split() + { + var halfWidth = _bounds.Width / 2; + var halfHeight = _bounds.Height / 2; + var halfDepth = _bounds.Depth / 2; + nodes[DNW] = otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y, _bounds.Z, halfWidth, halfHeight, + halfDepth, this); + nodes[DNE] = otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y, _bounds.Z, halfWidth, + halfHeight, halfDepth, this); + nodes[DSW] = otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y, _bounds.Z + halfDepth, halfWidth, + halfHeight, halfDepth, this); + nodes[DSE] = otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y, + _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth, this); + + nodes[UNW] = otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y + halfHeight, _bounds.Z, + halfWidth, halfHeight, halfDepth, this); + nodes[UNE] = otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y + halfHeight, + _bounds.Z, halfWidth, halfHeight, halfDepth, this); + nodes[USW] = otPool.Obtain().Init(treeDepth + 1, _bounds.X, _bounds.Y + halfHeight, + _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth, this); + nodes[USE] = otPool.Obtain().Init(treeDepth + 1, _bounds.X + halfWidth, _bounds.Y + halfHeight, + _bounds.Z + halfDepth, halfWidth, halfHeight, halfDepth, this); + } + + private void HandleMerge() + { + if (!ShouldMerge()) return; + + for (var index = nodes.Length - 1; index >= 0; index--) + { + var ocTree = nodes[index]; + for (var i = ocTree._containers.Size() - 1; i >= 0; i--) + { + var ocTreeContainer = ocTree._containers[i]; + _containersToAdd.Add(ocTreeContainer); + ocTree._containers.Remove(i); + } + } + + for (var index = nodes.Length - 1; index >= 0; index--) + { + var ocTree = nodes[index]; + nodes[index] = null; + otPool.Release(ocTree); + ocTree.Reset(); + } + + foreach (var container in _containersToAdd) Insert(container); + + _containersToAdd.Clear(); + } + + #region Node Bitwise + + private const int OUTSIDE = -1; + private const int D = 0b000; + private const int U = 0b100; + private const int S = 0b000; + private const int N = 0b010; + private const int W = 0b000; + private const int E = 0b001; + private const int DSW = D | S | W; // 0 + private const int DSE = D | S | E; // 1 + private const int DNW = D | N | W; // 2 + private const int DNE = D | N | E; // 3 + private const int USW = U | S | W; // 4 + private const int USE = U | S | E; // 5 + private const int UNW = U | N | W; // 6 + private const int UNE = U | N | E; // 7 + + #endregion + + #region Modifiers + + public void Upsert(long entityId, long flags, BoundingBoxD aabb) + { + Upsert(entityId, flags, (float)aabb.Min.X, (float)aabb.Min.Y, (float)aabb.Min.Z, + (float)aabb.Max.X - (float)aabb.Min.X, (float)aabb.Max.Y - (float)aabb.Min.Y, + (float)aabb.Max.Z - (float)aabb.Min.Z); + } + + public void Upsert(long entityId, float x, float y, float z, float width, float height, float depth) + { + Upsert(entityId, 0L, x, y, z, width, height, depth); + } + + public void Upsert(long entityId, long flags, float x, float y, float z, float width, float height, float depth) + { + Container container = null; + if (idToContainer.ContainsKey(entityId)) container = idToContainer[entityId]; + if (container != null && container.EntityId != -1) + { + container.Flags |= flags; + Update(entityId, x, y, z, width, height, depth); + return; + } + + Insert(entityId, flags, x, y, z, width, height, depth); + } + + private void Update(long entityId, float x, float y, float z, float width, float height, float depth) + { + var c = idToContainer[entityId]; + c.Set(entityId, c.Flags, x, y, z, width, height, depth); + + var parentTree = c.Parent; + if (parentTree._bounds.Contains(c)) return; + + parentTree._containers.Remove(c); + while (parentTree.parent != null && !parentTree._bounds.Contains(c)) parentTree = parentTree.parent; + + parentTree.Insert(c); + } + + private void Insert(long entityId, float x, float y, float z, float width, float height, float depth) + { + Insert(entityId, 0L, x, y, z, width, height, depth); + } + + private void Insert(long entityId, long flags, float x, float y, float z, float width, float height, + float depth) + { + Insert(cPool.Obtain().Set(entityId, flags, x, y, z, width, height, depth)); + } + + private void Insert(Container c) + { + if (nodes[0] != null) + { + var index = IndexOf(c.X, c.Y, c.Z, c.Width, c.Height, c.Depth); + if (index != OUTSIDE) + { + nodes[index].Insert(c); + return; + } + } + + c.Parent = this; + idToContainer[c.EntityId] = c; + _containers.Add(c); + + if (_containers.Size() <= capacity || treeDepth >= maxDepth) return; + + if (nodes[0] == null) Split(); + + var items = _containers.data; + for (var i = _containers.Size() - 1; i >= 0; i--) + { + var next = items[i]; + if (next == null) continue; + var index = IndexOf(next.X, next.Y, next.Z, next.Width, next.Height, next.Depth); + if (index == OUTSIDE) continue; + nodes[index].Insert(next); + _containers.Remove(i); + } + } + + public void Remove(long entityId) + { + if (!idToContainer.ContainsKey(entityId)) return; + + var container = idToContainer[entityId]; + var parent = container.Parent; + parent?._containers.Remove(container); + idToContainer.Remove(entityId); + cPool.Release(container); + parent?.parent?.HandleMerge(); + } + + #endregion + + #region Getters + + /// + /// Returns entityIds of entities that are inside the OcTrees that contain given point. + /// + /// List to fill + /// X position + /// Y position + /// Z position + public List Get(List entityIds, float x, float y, float z) + { + if (!_bounds.Contains(x, y, z)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, 0f, 0f, 0f); + if (index != OUTSIDE) nodes[index].Get(entityIds, x, y, z); + } + + entityIds.AddRange(_containers.Select(container => container.EntityId)); + + return entityIds; + } + + public List Get(List entityIds, float x, float y, float z, long flags) + { + if (flags == 0L) return Get(entityIds, x, y, z); + if (!_bounds.Contains(x, y, z)) return entityIds; + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, 0f, 0f, 0f); + if (index != OUTSIDE) nodes[index].Get(entityIds, x, y, z, flags); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) + select container.EntityId + ); + + return entityIds; + } + + public List Get(List entityIds, BoundingSphereD sphereD) + { + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].Get(entityIds, sphereD); + else + foreach (var ocTree in nodes) + ocTree.Get(entityIds, sphereD); + } + + entityIds.AddRange(_containers.Select(container => container.EntityId)); + + return entityIds; + } + + public List Get(List entityIds, long flags, BoundingSphereD sphereD) + { + if (flags == 0L) return Get(entityIds, sphereD); + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].Get(entityIds, flags, sphereD); + else + foreach (var ocTree in nodes) + ocTree.Get(entityIds, flags, sphereD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) + select container.EntityId); + + return entityIds; + } + + //TODO: Make exact check sphere rather than box + public List GetExact(List entityIds, BoundingSphereD sphereD) + { + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].GetExact(entityIds, sphereD); + else + foreach (var ocTree in nodes) + ocTree.GetExact(entityIds, sphereD); + } + + entityIds.AddRange(from container in _containers + where container.Overlaps(x, y, z, width, height, depth) && sphereD.Intersects( + new BoundingBoxD(new Vector3D(container.X, container.Y, container.Z), + new Vector3D( + container.X + container.Width, + container.Y + container.Height, + container.Z + container.Depth + ))) + select container.EntityId); + + return entityIds; + } + + public List GetExact(List entityIds, long flags, BoundingSphereD sphereD) + { + if (flags == 0L) return GetExact(entityIds, sphereD); + var box = sphereD.GetBoundingBox(); + var x = (float)box.Min.X; + var y = (float)box.Min.Y; + var z = (float)box.Min.Z; + var width = (float)box.Max.X - x; + var height = (float)box.Max.Y - y; + var depth = (float)box.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].GetExact(entityIds, flags, sphereD); + else + foreach (var ocTree in nodes) + ocTree.GetExact(entityIds, flags, sphereD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) && container.Overlaps(x, y, z, width, height, depth) && + sphereD.Intersects( + new BoundingBoxD(new Vector3D(container.X, container.Y, container.Z), + new Vector3D( + container.X + container.Width, + container.Y + container.Height, + container.Z + container.Depth + ))) + select container.EntityId); + + return entityIds; + } + + public List Get(List entityIds, BoundingBoxD boxD) + { + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].Get(entityIds, boxD); + else + foreach (var ocTree in nodes) + ocTree.Get(entityIds, boxD); + } + + entityIds.AddRange(_containers.Select(container => container.EntityId)); + + return entityIds; + } + + public List Get(List entityIds, long flags, BoundingBoxD boxD) + { + if (flags == 0L) return Get(entityIds, boxD); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].Get(entityIds, flags, boxD); + else + foreach (var ocTree in nodes) + ocTree.Get(entityIds, flags, boxD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) && container.Overlaps(x, y, z, width, height, depth) + select container.EntityId); + + return entityIds; + } + + public List GetExact(List entityIds, MyOrientedBoundingBoxD boxBoundingBox) + { + var boxD = boxBoundingBox.GetAABB(); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].GetExact(entityIds, boxD); + else + foreach (var ocTree in nodes) + ocTree.GetExact(entityIds, boxD); + } + + foreach (var container in _containers) + { + if (!container.Overlaps(x, y, z, width, height, depth)) continue; + + var containerBox = container.BoundingBox; + if (boxBoundingBox.Intersects(ref containerBox)) entityIds.Add(container.EntityId); + } + + return entityIds; + } + + public List GetExact(List entityIds, long flags, MyOrientedBoundingBoxD boxBoundingBox) + { + if (flags == 0L) return GetExact(entityIds, boxBoundingBox); + var boxD = boxBoundingBox.GetAABB(); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].GetExact(entityIds, boxD); + else + foreach (var ocTree in nodes) + ocTree.GetExact(entityIds, boxD); + } + + foreach (var container in _containers) + { + if (!container.MatchesFlags(flags) || !container.Overlaps(x, y, z, width, height, depth)) continue; + + var containerBox = container.BoundingBox; + if (boxBoundingBox.Intersects(ref containerBox)) entityIds.Add(container.EntityId); + } + + return entityIds; + } + + public List GetExact(List entityIds, BoundingBoxD boxD) + { + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].GetExact(entityIds, boxD); + else + foreach (var ocTree in nodes) + ocTree.GetExact(entityIds, boxD); + } + + entityIds.AddRange(from container in _containers + where container.Overlaps(x, y, z, width, height, depth) + select container.EntityId); + + return entityIds; + } + + public List GetExact(List entityIds, long flags, BoundingBoxD boxD) + { + if (flags == 0L) return GetExact(entityIds, boxD); + var x = (float)boxD.Min.X; + var y = (float)boxD.Min.Y; + var z = (float)boxD.Min.Z; + var width = (float)boxD.Max.X - x; + var height = (float)boxD.Max.Y - y; + var depth = (float)boxD.Max.Z - z; + + if (!_bounds.Overlaps(x, y, z, width, height, depth)) return entityIds; + + if (nodes[0] != null) + { + var index = IndexOf(x, y, z, width, height, depth); + if (index != OUTSIDE) nodes[index].GetExact(entityIds, flags, boxD); + else + foreach (var ocTree in nodes) + ocTree.GetExact(entityIds, flags, boxD); + } + + entityIds.AddRange(from container in _containers + where container.MatchesFlags(flags) && container.Overlaps(x, y, z, width, height, depth) + select container.EntityId); + + return entityIds; + } + + #endregion + } + + public class Container : IPoolable + { + public long EntityId; + public long Flags; + public OcTree Parent; + public float X { get; private set; } + public float Y { get; private set; } + public float Z { get; private set; } + public float Width { get; private set; } + public float Height { get; private set; } + public float Depth { get; private set; } + + public BoundingBoxD BoundingBox => + new BoundingBoxD(new Vector3(X, Y, Z), new Vector3(X + Width, Y + Height, Z + Depth)); + + public void Reset() + { + EntityId = -1; + Flags = 0; + X = 0; + Y = 0; + Z = 0; + Width = 0; + Height = 0; + Depth = 0; + } + + public Container Set(long entityId, long flags, float x, float y, float z, float width, float height, + float depth) + { + EntityId = entityId; + Flags = flags; + X = x; + Y = y; + Z = z; + Width = width; + Height = height; + Depth = depth; + return this; + } + + public Container Set(float x, float y, float z, float width, float height, float depth) + { + X = x; + Y = y; + Z = z; + Width = width; + Height = height; + Depth = depth; + return this; + } + + public bool MatchesFlags(long flags) + { + return (Flags & flags) > 0; + } + + public bool Contains(float x, float y, float z) + { + return X <= x && x <= X + Width && + Y <= y && y <= Y + Height && + Z <= z && z <= Z + Depth; + } + + public bool Contains(float x, float y, float z, float width, float height, float depth) + { + var xMax = x + width; + var yMax = x + height; + var zMax = x + depth; + + return x > X && x < X + Width && xMax > X && xMax < X + Width && + y > Y && y < Y + Height && yMax > Y && yMax < Y + Height && + z > Z && z < Z + Depth && zMax > Z && zMax < Z + Depth; + } + + public bool Overlaps(float x, float y, float z, float width, float height, float depth) + { + if (X > x + width) return false; + if (x > X + Width) return false; + if (Y > y + height) return false; + if (y > Y + Height) return false; + if (Z > z + depth) return false; + if (z > Z + Depth) return false; + + return true; + } + + public bool Contains(Container c) + { + return Contains(c.X, c.Y, c.Z, c.Width, c.Height, c.Depth); + } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/OcTreeHandler.cs b/GlobalShared/OcTree/OcTreeHandler.cs new file mode 100644 index 0000000..b510217 --- /dev/null +++ b/GlobalShared/OcTree/OcTreeHandler.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.Linq; +using Global.Patches; +using Global.Shared.API; +using Global.Shared.Events; +using Global.Shared.OcTree.Data; +using Global.Shared.Plugin; +using Sandbox.Game.Entities; +using VRage.Game.Components; +using VRage.Game.Entity; +using VRage.Game.ModAPI; + +namespace Global.Shared.OcTree +{ + public class OcTreeHandler + { + public GenericOcTree GridTree { get; private set; } + + public GenericOcTree CharacterTree { get; private set; } + + internal void Init() + { + var cfg = GlobalInstance.Config; + var offset = cfg.CenterPosition; + var x = cfg.Size; + var y = cfg.Size; + var z = cfg.Size; + GridTree = new GenericOcTree(offset.X - x, offset.Y - y, offset.Z - z, x * 2, y * 2, z * 2, + cfg.Capacity, cfg.MaxDepth); + CharacterTree = + new GenericOcTree(offset.X - x, offset.Y - y, offset.Z - z, x * 2, y * 2, z * 2, + cfg.Capacity, cfg.MaxDepth); + + BlockEvents.OnPlayerControlAcquired += AddControllableEntity; + BlockEvents.OnPlayerControlReleased += RemoveControllableEntity; + } + + public IGridData AddGrid(MyCubeGrid grid) + { + var bigOwner = grid.BigOwners.Count == 0 ? "No Owner!" : grid.BigOwners.First().ToString(); + // check if grid is new subgrid + if (MyMechanicalConnectionBlockBasePatch.IsCreatingSubPart) + { + GlobalInstance.Log.Trace( + $"Grid is subgrid, ignoring {grid.DisplayName} {grid.PositionComp.GetPosition()} {bigOwner}"); + return null; + } + + if (!GridTree.Contains(grid.EntityId)) + { + var grids = grid.GetGridGroup(GridLinkTypeEnum.Mechanical).GetGrids(new List()); + var largestGrid = grid; + if (grids.Count > 1) + foreach (var g in grids.Cast()) + if (g.BlocksCount > largestGrid.BlocksCount) + largestGrid = g; + + grid = largestGrid; + //GlobalInstance.Log.Info( + //$"Adding grid to OcTree: {grid.EntityId} {grid.DisplayName} {grid.PositionComp.GetPosition()}"); + + var data = new GridData(grid); + grid.PositionComp.OnPositionChanged += Grid_OnPositionChanged; + grid.OnMarkForClose += RemoveGrid; + grid.OnTeleported += Grid_OnChanged; + GridTree.Insert(data); + grid.OnConnectionChanged += Grid_OnConnectionChanged; + + GridEvents.GridCreated?.Invoke(grid); + return data; + } + + GlobalInstance.Log.Debug( + $"Attempted to add grid to OcTree that already exists, {grid.DisplayName} {grid.PositionComp.GetPosition()} {bigOwner}"); + + var existing = GridTree[grid.EntityId]; + + if (existing != null) return existing; + + GridTree.Remove(grid.EntityId); + return AddGrid(grid); + } + + public void AddControllableEntity(IMyControllableEntity entity) + { + if (entity.ControllerInfo?.Controller?.Player == null && !(entity is IMyCharacter)) + //GlobalInstance.Log.Info($"Entity is not controlled {entity.Entity.EntityId} {entity.Entity.DisplayName}"); + return; + //GlobalInstance.Log.Debug($"AddControllableEntity {entity.Entity.EntityId} {entity.Entity.DisplayName}"); + entity.Entity.OnMarkForClose += RemoveControllableEntity; + + var data = new ControllableEntityData(entity); + + if (!CharacterTree.Contains(entity.Entity.EntityId)) CharacterTree.Insert(data); + } + + public void RemoveControllableEntity(IMyControllableEntity entity) + { + RemoveControllableEntity((MyEntity)entity); + } + + public void RemoveControllableEntity(MyEntity entity) + { + //GlobalInstance.Log.Debug("RemoveControllableEntity"); + CharacterTree.Remove(entity.EntityId); + } + + private void Grid_OnConnectionChanged(MyCubeGrid arg1, GridLinkTypeEnum arg2) + { + if (arg2 != GridLinkTypeEnum.Mechanical) return; + + GridTree.Update(arg1.EntityId); + } + + internal void RemoveGrid(MyEntity grid) + { + GlobalInstance.Log.Debug($"Removing grid from OcTree: {grid.EntityId} {grid.DisplayName}"); + GridEvents.GridRemoved?.Invoke(grid as MyCubeGrid); + + grid.PositionComp.OnPositionChanged -= Grid_OnPositionChanged; + grid.OnMarkForClose -= RemoveGrid; + grid.OnTeleported -= Grid_OnChanged; + + GridTree.Remove(grid.EntityId); + } + + private void Grid_OnChanged(MyEntity obj) + { + GridTree.Update(obj.EntityId); + } + + private void Grid_OnPositionChanged(MyPositionComponentBase obj) + { + if (!(obj.Entity is MyCubeGrid grid)) return; + if (grid.Closed || grid.MarkedForClose) return; + + GridTree.Update(grid.EntityId); + } + } +} \ No newline at end of file diff --git a/GlobalShared/OcTree/Pool.cs b/GlobalShared/OcTree/Pool.cs new file mode 100644 index 0000000..7df836c --- /dev/null +++ b/GlobalShared/OcTree/Pool.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace Global.Shared.OcTree +{ + public interface IPoolable + { + void Reset(); + } + + public class Pool where E : IPoolable + { + private readonly Func creator; + private readonly List pool = new List(); + + public Pool(Func creator) + { + this.creator = creator; + } + + public E Obtain() + { + if (pool.Count <= 0) return creator(); + + var e = pool[0]; + pool.RemoveAt(0); + return e; + } + + public void Release(E e) + { + e.Reset(); + pool.Add(e); + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/CubeGridPatch.cs b/GlobalShared/Patches/CubeGridPatch.cs new file mode 100644 index 0000000..73b1b7f --- /dev/null +++ b/GlobalShared/Patches/CubeGridPatch.cs @@ -0,0 +1,66 @@ +using System.Threading; +using Global.Shared.Events; +using HarmonyLib; +using Sandbox.Definitions; +using Sandbox.Game.Entities; +using VRageMath; + +namespace Global.Shared.Patches +{ + [HarmonyPatch(typeof(MyCubeGrid))] + public class CubeGridPatch + { + private static readonly ThreadLocal CallDepth = new ThreadLocal(); + + public static ThreadLocal PlayerId = new ThreadLocal(); + public static bool IsInMergeGridInternal => CallDepth.Value > 0; + + [HarmonyPrefix] + [HarmonyPatch("MergeGridInternal")] + private static bool MergeGridInternalPrefix() + { + // if (Config.Patches.DisableAllPatches || !Config.Patches.EnableGridMerge) return true; + + CallDepth.Value++; + + return true; + } + + [HarmonyPostfix] + [HarmonyPatch("MergeGridInternal")] + private static void MergeGridInternalPostfix(MyCubeGrid __instance) + { + if (!IsInMergeGridInternal) return; + if (--CallDepth.Value > 0) return; + __instance.GridSystems.ConveyorSystem.FlagForRecomputation(); + } + + [HarmonyPatch("CanPlaceBlock", typeof(Vector3I), typeof(Vector3I), typeof(MyBlockOrientation), + typeof(MyCubeBlockDefinition), typeof(ulong), typeof(int?), typeof(bool), typeof(bool))] + [HarmonyPostfix] + private static void CanPlaceBlockPostfix(Vector3I min, + Vector3I max, + MyBlockOrientation orientation, + MyCubeBlockDefinition definition, + ulong placingPlayer = 0, + int? ignoreMultiblockId = null, + bool ignoreFracturedPieces = false, + bool isProjection = false) + { + PlayerId.Value = placingPlayer; + } + + [HarmonyPatch("CanAddCubes", typeof(Vector3I), typeof(Vector3I), typeof(MyBlockOrientation), + typeof(MyCubeBlockDefinition))] + [HarmonyPostfix] + private static bool CanAddCubesPostfix(bool canPlace, MyCubeGrid __instance, Vector3I min, + Vector3I max, + MyBlockOrientation? orientation, + MyCubeBlockDefinition definition) + { + BlockEvents.OnCanPlaceBlockEvent(__instance, orientation, definition, PlayerId.Value, ref canPlace); + PlayerId.Value = 0; + return canPlace; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/EntityNamePatch.cs b/GlobalShared/Patches/EntityNamePatch.cs new file mode 100644 index 0000000..95fea3f --- /dev/null +++ b/GlobalShared/Patches/EntityNamePatch.cs @@ -0,0 +1,85 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using HarmonyLib; +using Sandbox.Game.Entities; +using VRage.Game.Entity; + +namespace Global.Shared.Patches +{ + [HarmonyPatch(typeof(MyEntities))] + public class EntityNamePatch + { + public static ConcurrentDictionary EntityNameReverseLookup = + new ConcurrentDictionary(); + // reverse dictionary + + [HarmonyPatch("SetEntityName")] + [HarmonyPrefix] + private static bool SetEntityNamePrefix(MyEntity myEntity, bool possibleRename) + { + if (string.IsNullOrEmpty(myEntity.Name)) + return false; + string previousName = null; + if (possibleRename) + if (EntityNameReverseLookup.ContainsKey(myEntity.EntityId)) + { + previousName = EntityNameReverseLookup[myEntity.EntityId]; + if (previousName != myEntity.Name) MyEntities.m_entityNameDictionary.Remove(previousName); + } + + if (MyEntities.m_entityNameDictionary.TryGetValue(myEntity.Name, out var myEntity1)) + { + if (myEntity1 == myEntity) + return false; + } + else + { + MyEntities.m_entityNameDictionary[myEntity.Name] = myEntity; + EntityNameReverseLookup[myEntity.EntityId] = myEntity.Name; + } + + // MyEntitiesButBetter.CallRename(myEntity, previousName, myEntity.Name); + + return false; + } + + [HarmonyPatch("RemoveName")] + [HarmonyPrefix] + private static bool RemoveNamePrefix(MyEntity entity) + { + if (string.IsNullOrEmpty(entity.Name)) + return false; + MyEntities.m_entityNameDictionary.Remove(entity.Name); + EntityNameReverseLookup.Remove(entity.EntityId); + return false; + } + + [HarmonyPatch("IsNameExists")] + [HarmonyPrefix] + private static bool IsNameExistsPrefix(ref bool __result, MyEntity entity, string name) + { + if (string.IsNullOrEmpty(entity.Name)) + { + __result = false; + return false; + } + + if (MyEntities.m_entityNameDictionary.ContainsKey(name)) + { + var ent = MyEntities.m_entityNameDictionary[entity.Name]; + __result = ent != entity; + return false; + } + + __result = false; + return false; + } + + [HarmonyPatch("UnloadData")] + [HarmonyPostfix] + private static void UnloadDataPostfix() + { + EntityNameReverseLookup.Clear(); + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/MyConveyorLinePatch.cs b/GlobalShared/Patches/MyConveyorLinePatch.cs new file mode 100644 index 0000000..1696fce --- /dev/null +++ b/GlobalShared/Patches/MyConveyorLinePatch.cs @@ -0,0 +1,17 @@ +using Global.Shared.Patches; +using HarmonyLib; +using Sandbox.Game.GameSystems.Conveyors; + +namespace Global.Patches +{ + [HarmonyPatch(typeof(MyConveyorLine))] + public class MyConveyorLinePatch + { + [HarmonyPrefix] + [HarmonyPatch(nameof(MyConveyorLine.UpdateIsWorking))] + private static bool UpdateIsWorkingPrefix() + { + return !CubeGridPatch.IsInMergeGridInternal; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/MyGamePruningStructurePatch.cs b/GlobalShared/Patches/MyGamePruningStructurePatch.cs new file mode 100644 index 0000000..26feb1b --- /dev/null +++ b/GlobalShared/Patches/MyGamePruningStructurePatch.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using Global.Shared.API; +using HarmonyLib; +using Sandbox.Game.Entities; +using VRage.Game.Entity; +using VRageMath; + +namespace Global.Shared.Patches +{ + [HarmonyPatch(typeof(MyGamePruningStructure))] + public class MyGamePruningStructurePatch + { + private static readonly List _gridQueryStorage = new List(); + private static readonly List _queryStorage = new List(); + + // [HarmonyPatch("GetTopMostEntitiesInBox")] + // [HarmonyPrefix] + public static bool GetTopMostEntitiesInBox( + ref BoundingBoxD box, + List result, + MyEntityQueryType qtype = MyEntityQueryType.Both) + { + GetTopMostBox(ref box, result, qtype); + return false; + } + + public static void GetTopMostBox(ref BoundingBoxD box, List result, MyEntityQueryType qType) + { + _queryStorage.Clear(); + _gridQueryStorage.Clear(); + var gridSearch = GridFlag.None; + if (qType.HasDynamic()) gridSearch |= GridFlag.DynamicGrid; + if (qType.HasStatic()) gridSearch |= GridFlag.StaticGrid; + if (gridSearch != GridFlag.None) GlobalAPI.LocateGrids(_gridQueryStorage, box, gridSearch); + + result.AddRange(_gridQueryStorage.Select(e => e.CubeGrid)); + + if (qType.HasDynamic()) GlobalAPI.LocateCharacters(_queryStorage, box, PlayerFlag.Character); + + result.AddRange(_queryStorage.Select(l => l.Entity.Entity)); + } + + // [HarmonyPatch("GetAllTopMostStaticEntitiesInBox")] + // [HarmonyPrefix] + public static bool GetAllTopMostStaticEntitiesInBox( + ref BoundingBoxD box, + List result, + MyEntityQueryType qtype = MyEntityQueryType.Both) + { + GetTopMostBox(ref box, result, qtype); + return false; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/MyMechanicalConnectionBlockBasePatch.cs b/GlobalShared/Patches/MyMechanicalConnectionBlockBasePatch.cs new file mode 100644 index 0000000..15c5ab5 --- /dev/null +++ b/GlobalShared/Patches/MyMechanicalConnectionBlockBasePatch.cs @@ -0,0 +1,33 @@ +using Global.Shared.Plugin; +using HarmonyLib; +using Sandbox.Game.Entities.Blocks; + +namespace Global.Patches +{ + [HarmonyPatch(typeof(MyMechanicalConnectionBlockBase))] + public class MyMechanicalConnectionBlockBasePatch + { + public static bool IsCreatingSubPart; + + [HarmonyPatch("RaiseAttachedEntityChanged")] + [HarmonyPrefix] + public static void RaiseAttachedEntityChanged(MyMechanicalConnectionBlockBase __instance) + { + GlobalStatic.OcTreeHandler.GridTree.Get(__instance.CubeGrid.EntityId)?.RaiseAttachedEntityChanged(); + } + + [HarmonyPatch("CreateTopPartAndAttach")] + [HarmonyPrefix] + public static void CreateTopPrefix(long builtBy, bool smallToLarge, bool instantBuild) + { + IsCreatingSubPart = true; + } + + [HarmonyPatch("CreateTopPartAndAttach")] + [HarmonyPostfix] + public static void CreateTopPostfix(long builtBy, bool smallToLarge, bool instantBuild) + { + IsCreatingSubPart = false; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/MyTerminalBlockPatch.cs b/GlobalShared/Patches/MyTerminalBlockPatch.cs new file mode 100644 index 0000000..776485c --- /dev/null +++ b/GlobalShared/Patches/MyTerminalBlockPatch.cs @@ -0,0 +1,24 @@ +using HarmonyLib; +using Sandbox.Game.Entities.Cube; +using VRage.Game; + +namespace Global.Patches +{ + [HarmonyPatch(typeof(MyTerminalBlock))] + public class MyTerminalBlockPatch + { + [HarmonyPatch("HasPlayerAccess")] + [HarmonyPrefix] + public static bool HasPlayerAccessPrefix(bool __result, long identityId, + MyRelationsBetweenPlayerAndBlock defaultNoUser = MyRelationsBetweenPlayerAndBlock.NoOwnership) + { + if (identityId != 0) return true; + + var defaultIsEnemyNeutralOrNoOwnership = defaultNoUser == MyRelationsBetweenPlayerAndBlock.Enemies || + defaultNoUser == MyRelationsBetweenPlayerAndBlock.Neutral || + defaultNoUser == MyRelationsBetweenPlayerAndBlock.NoOwnership; + __result = !defaultIsEnemyNeutralOrNoOwnership; + return false; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patches/TerminalSystemPatch.cs b/GlobalShared/Patches/TerminalSystemPatch.cs new file mode 100644 index 0000000..480c21f --- /dev/null +++ b/GlobalShared/Patches/TerminalSystemPatch.cs @@ -0,0 +1,31 @@ +using System.Runtime.CompilerServices; +using Global.Shared.Util; +using HarmonyLib; +using Sandbox.Game.GameSystems; + +namespace Global.Shared.Patches +{ + [HarmonyPatch(typeof(MyGridTerminalSystem))] + public class TerminalSystemPatch + { + public static readonly UintCache OwnerCache = new UintCache(337 * 60); + + [HarmonyPrefix] + [HarmonyPatch(nameof(MyGridTerminalSystem.UpdateGridBlocksOwnership))] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool UpdateGridBlocksOwnershipPrefix(MyGridTerminalSystem __instance, long ownerID) + { + var key = __instance.GetHashCode() ^ ownerID; + if (OwnerCache.TryGetValue(key, out var value)) + { + if (value == (uint)ownerID) return false; + + OwnerCache.Forget(key); + return true; + } + + OwnerCache.Store(key, (uint)ownerID, 240 + ((uint)key & 63)); + return true; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Patching/PatchHelpers.cs b/GlobalShared/Patching/PatchHelpers.cs new file mode 100644 index 0000000..cb48e01 --- /dev/null +++ b/GlobalShared/Patching/PatchHelpers.cs @@ -0,0 +1,27 @@ +using System; +using System.Reflection; +using Global.Shared.Logging; +using HarmonyLib; + +namespace Global.Shared.Patching +{ + public static class PatchHelpers + { + public static bool PatchAll(IPluginLogger log, Harmony harmony) + { + log.Info("Applying patches..."); + + try + { + harmony.PatchAll(Assembly.GetCallingAssembly()); + } + catch (Exception ex) + { + log.Critical(ex, "Failed to apply patches!"); + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Plugin/GlobalInstance.cs b/GlobalShared/Plugin/GlobalInstance.cs new file mode 100644 index 0000000..d8fbacb --- /dev/null +++ b/GlobalShared/Plugin/GlobalInstance.cs @@ -0,0 +1,24 @@ +using System; +using Global.Shared.Config; +using Global.Shared.Logging; + +namespace Global.Shared.Plugin +{ + public static class GlobalInstance + { + public static IPluginLogger Log => Plugin.Log; + public static IGlobalPlugin Plugin { get; private set; } + public static IPluginConfig Config => Plugin.Config; + + public static void SetPlugin(IGlobalPlugin plugin) + { + Plugin = plugin; + GlobalStatic.OcTreeHandler.Init(); + } + + public static void Run(Action action) + { + Plugin.Run(action); + } + } +} \ No newline at end of file diff --git a/GlobalShared/Plugin/GlobalStatic.cs b/GlobalShared/Plugin/GlobalStatic.cs new file mode 100644 index 0000000..0538ceb --- /dev/null +++ b/GlobalShared/Plugin/GlobalStatic.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Global.Shared.OcTree; +using Global.Shared.Patches; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using VRage.ModAPI; + +namespace Global.Shared.Plugin +{ + public static class GlobalStatic + { + public static OcTreeHandler OcTreeHandler = new OcTreeHandler(); + + internal static void Init() + { + MyAPIGateway.Entities.OnEntityAdd += Entities_OnEntityAdd; + var set = new HashSet(); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + MyAPIGateway.Entities.GetEntities(set); + foreach (var myEntity in set) Entities_OnEntityAdd(myEntity); + stopwatch.Stop(); + GlobalInstance.Log.Info($"Collected all entities from startup. Took {stopwatch.ElapsedMilliseconds}ms"); + } + + internal static void Update() + { + TerminalSystemPatch.OwnerCache.Cleanup(); + } + + private static void Entities_OnEntityAdd(IMyEntity obj) + { + if (obj is MyCubeGrid grid) + OcTreeHandler.AddGrid(grid); + else if (obj is IMyControllableEntity controllableEntity) + OcTreeHandler.AddControllableEntity(controllableEntity); + } + } +} \ No newline at end of file diff --git a/GlobalShared/Plugin/IGlobalPlugin.cs b/GlobalShared/Plugin/IGlobalPlugin.cs new file mode 100644 index 0000000..077d2ad --- /dev/null +++ b/GlobalShared/Plugin/IGlobalPlugin.cs @@ -0,0 +1,15 @@ +using System; +using Global.Shared.Config; +using Global.Shared.Logging; + +namespace Global.Shared.Plugin +{ + public interface IGlobalPlugin + { + IPluginLogger Log { get; } + IPluginConfig Config { get; } + long Tick { get; } + bool Started { get; } + void Run(Action action); + } +} \ No newline at end of file diff --git a/GlobalShared/Properties/AssemblyInfo.cs b/GlobalShared/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..40de19f --- /dev/null +++ b/GlobalShared/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GlobalShared")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GlobalShared")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("C04593FB-B399-43C1-83C5-E1262A3E0465")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/GlobalShared/Util/KeenExtensions.cs b/GlobalShared/Util/KeenExtensions.cs new file mode 100644 index 0000000..78643ca --- /dev/null +++ b/GlobalShared/Util/KeenExtensions.cs @@ -0,0 +1,19 @@ +using Global.Shared.OcTree; +using VRageMath; + +namespace Global.Shared.Util +{ + public static class KeenExtensions + { + public static BoundingBoxD SetToContainer(this BoundingBoxD box, IContainer container) + { + box.Min.X = container.X; + box.Min.Y = container.Y; + box.Min.Z = container.Z; + box.Max.X = container.X + container.Width; + box.Max.Y = container.Y + container.Height; + box.Max.Z = container.Z + container.Depth; + return box; + } + } +} \ No newline at end of file diff --git a/GlobalShared/Util/NexusUtils.cs b/GlobalShared/Util/NexusUtils.cs new file mode 100644 index 0000000..39e3f7e --- /dev/null +++ b/GlobalShared/Util/NexusUtils.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; + +using Nexus.API; +using Nexus.Utilities; +using Sandbox.Engine.Multiplayer; +using Sandbox.ModAPI; +using ServerNetwork.Sync; +using VRageMath; + +namespace Global.Shared.Util +{ + public class NexusUtils + { + public static NexusServerSideAPI nexusServerAPI = new NexusServerSideAPI(1234); + public static NexusAPI nexusAPI = new NexusAPI((ushort)nexusServerAPI.CrossServerModID); + + + public void BroadCastMessage(string author, string message, Color color) + { + var scriptedChatMsg1 = new ScriptedChatMsg + { + Author = author, + Text = message, + Font = "White", + Color = color, + Target = 0 + }; + + var msg = new ScriptedMessage + { + Author = scriptedChatMsg1.Author, + ChatColor = scriptedChatMsg1.Color.GetDrawingColor().ToArgb(), + Target = scriptedChatMsg1.Target, + Text = scriptedChatMsg1.Text + }; + NexusServerSideAPI.SendMessageToAllServers(ref nexusAPI, + MyAPIGateway.Utilities.SerializeToBinary(msg)); + } + } +} diff --git a/GlobalShared/Util/UintCache.cs b/GlobalShared/Util/UintCache.cs new file mode 100644 index 0000000..e01412e --- /dev/null +++ b/GlobalShared/Util/UintCache.cs @@ -0,0 +1,93 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Global.Shared.Plugin; + +namespace Global.Shared.Util +{ + public class UintCache + { + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + private readonly ulong cleanupPeriod; + private readonly TK[] keysToDelete; + private readonly uint maxDeleteCount; + private ulong nextCleanup; + private ulong tick; + + public UintCache(uint cleanupPeriod, uint maxDeleteCount = 64) + { + this.cleanupPeriod = (ulong)cleanupPeriod << 32; + this.maxDeleteCount = maxDeleteCount; + + keysToDelete = new TK[this.maxDeleteCount]; + nextCleanup = tick + cleanupPeriod; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Cleanup() + { + if (GlobalInstance.Plugin == null) return; + if ((tick = (ulong)GlobalInstance.Plugin.Tick << 32) < nextCleanup) + return; + + nextCleanup = tick + cleanupPeriod; + + var count = 0u; + foreach (var (key, item) in cache) + { + if (item >= tick) + continue; + + keysToDelete[count++] = key; + if (count == maxDeleteCount) + break; + } + + if (count == 0) + return; + + for (var i = 0; i < count; i++) + cache.Remove(keysToDelete[i]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + cache.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Store(TK key, uint value, uint lifetime) + { + var expires = tick + ((ulong)lifetime << 32); + cache[key] = expires | value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Extend(TK key, uint lifetime) + { + if (cache.TryGetValue(key, out var item)) + cache[key] = (tick + ((ulong)lifetime << 32)) | (item & 0xfffffffful); + } + + public void Forget(TK key) + { + cache.Remove(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TK key, out uint value) + { + if (cache.TryGetValue(key, out var item)) + if (item >= tick) + { + value = (uint)item; + return true; + } + + value = 0u; + return false; + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Discord/DiscordStructs.cs b/GlobalTorch/API/Discord/DiscordStructs.cs new file mode 100644 index 0000000..7775b6b --- /dev/null +++ b/GlobalTorch/API/Discord/DiscordStructs.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using Newtonsoft.Json; + +namespace Global.API.Discord +{ + /// + /// Discord message data object + /// + public struct DiscordMessage + { + /// + /// Message content + /// + public string Content; + + /// + /// Read message to everyone on the channel + /// + public bool TTS; + + public DiscordAllowedMentions AllowedMentions; + + /// + /// Webhook profile username to be shown + /// + public string Username; + + /// + /// Webhook profile avater to be shown + /// + public string AvatarUrl; + + /// + /// List of embeds + /// + public List Embeds; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + public struct DiscordAllowedMentions + { + public List Parse; + public List Roles; + public List Users; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + /// + /// Discord embed data object + /// + public struct DiscordEmbed + { + /// + /// Embed title + /// + public string Title; + + /// + /// Embed description + /// + public string Description; + + /// + /// Embed url + /// + public string Url; + + /// + /// Embed timestamp + /// + public DateTime? Timestamp; + + /// + /// Embed color + /// + public Color? Color; + + /// + /// Embed footer + /// + public EmbedFooter? Footer; + + /// + /// Embed image + /// + public EmbedMedia? Image; + + /// + /// Embed thumbnail + /// + public EmbedMedia? Thumbnail; + + /// + /// Embed video + /// + public EmbedMedia? Video; + + /// + /// Embed provider + /// + public EmbedProvider? Provider; + + /// + /// Embed author + /// + public EmbedAuthor? Author; + + /// + /// Embed fields list + /// + public List Fields; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + /// + /// Discord embed footer data object + /// + public struct EmbedFooter + { + /// + /// Footer text + /// + public string Text; + + /// + /// Footer icon + /// + public string IconUrl; + + /// + /// Footer icon proxy + /// + public string ProxyIconUrl; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + /// + /// Discord embed media data object (images/thumbs/videos) + /// + public struct EmbedMedia + { + /// + /// Media url + /// + public string Url; + + /// + /// Media proxy url + /// + public string ProxyUrl; + + /// + /// Media height + /// + public int? Height; + + /// + /// Media width + /// + public int? Width; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + /// + /// Discord embed provider data object + /// + public struct EmbedProvider + { + /// + /// Provider name + /// + public string Name; + + /// + /// Provider url + /// + public string Url; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + /// + /// Discord embed author data object + /// + public struct EmbedAuthor + { + /// + /// Author name + /// + public string Name; + + /// + /// Author url + /// + public string Url; + + /// + /// Author icon + /// + public string IconUrl; + + /// + /// Author icon proxy + /// + public string ProxyIconUrl; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } + + /// + /// Discord embed field data object + /// + public struct EmbedField + { + /// + /// Field name + /// + public string Name; + + /// + /// Field value + /// + public string Value; + + /// + /// Field align + /// + public bool InLine; + + public override string ToString() + { + return DiscordUtil.StructToJson(this).ToString(Formatting.None); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Discord/DiscordUtil.cs b/GlobalTorch/API/Discord/DiscordUtil.cs new file mode 100644 index 0000000..13a0923 --- /dev/null +++ b/GlobalTorch/API/Discord/DiscordUtil.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace Global.API.Discord +{ + public static class DiscordUtil + { + private static readonly string[] ignore = { "InLine" }; + + /// + /// Convert Color object into hex integer + /// + /// Color to be converted + /// Converted hex integer + public static int ColorToHex(Color color) + { + var HS = + color.R.ToString("X2") + + color.G.ToString("X2") + + color.B.ToString("X2"); + + return int.Parse(HS, NumberStyles.HexNumber); + } + + internal static JObject StructToJson(object @struct) + { + var type = @struct.GetType(); + var json = new JObject(); + + var fields = type.GetFields(); + foreach (var field in fields) + { + var name = FieldNameToJsonName(field.Name); + var value = field.GetValue(@struct); + if (value == null) + continue; + + if (value is bool b) + { + json.Add(name, b); + } + else if (value is int i) + { + json.Add(name, i); + } + else if (value is Color color) + { + json.Add(name, ColorToHex(color)); + } + else if (value is string s) + { + json.Add(name, s); + } + else if (value is DateTime time) + { + json.Add(name, time.ToString("yyyy-MM-ddTHH\\:mm\\:ss.fffffffzzz")); + } + else if (value is IList list && list.GetType().IsGenericType) + { + var array = new JArray(); + foreach (var obj in list) + if (obj is string str) + array.Add(str); + else + array.Add(StructToJson(obj)); + + json.Add(name, array); + } + else + { + json.Add(name, StructToJson(value)); + } + } + + return json; + } + + internal static string FieldNameToJsonName(string name) + { + if (ignore.ToList().Contains(name)) + return name.ToLower(); + + var result = new List(); + + if (IsFullUpper(name)) + result.AddRange(name.ToLower().ToCharArray()); + else + for (var i = 0; i < name.Length; i++) + if (i > 0 && char.IsUpper(name[i])) + result.AddRange(new[] { '_', char.ToLower(name[i]) }); + else result.Add(char.ToLower(name[i])); + + return string.Join("", result); + } + + internal static bool IsFullUpper(string str) + { + var upper = true; + for (var i = 0; i < str.Length; i++) + if (!char.IsUpper(str[i])) + { + upper = false; + break; + } + + return upper; + } + + public static string Decode(Stream source) + { + using (var reader = new StreamReader(source)) + { + return reader.ReadToEnd(); + } + } + + public static byte[] Encode(string source, string encoding = "utf-8") + { + return Encoding.GetEncoding(encoding).GetBytes(source); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Discord/DiscordWebhook.cs b/GlobalTorch/API/Discord/DiscordWebhook.cs new file mode 100644 index 0000000..6ce3df7 --- /dev/null +++ b/GlobalTorch/API/Discord/DiscordWebhook.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Net; + +namespace Global.API.Discord +{ + public class DiscordWebhook + { + /// + /// Webhook url + /// + public string Url { get; set; } + + private void AddField(MemoryStream stream, string bound, string cDisposition, string cType, byte[] data) + { + var prefix = stream.Length > 0 ? "\r\n--" : "--"; + var fBegin = $"{prefix}{bound}\r\n"; + + var fBeginBuffer = DiscordUtil.Encode(fBegin); + var cDispositionBuffer = DiscordUtil.Encode(cDisposition); + var cTypeBuffer = DiscordUtil.Encode(cType); + + stream.Write(fBeginBuffer, 0, fBeginBuffer.Length); + stream.Write(cDispositionBuffer, 0, cDispositionBuffer.Length); + stream.Write(cTypeBuffer, 0, cTypeBuffer.Length); + stream.Write(data, 0, data.Length); + } + + private void SetJsonPayload(MemoryStream stream, string bound, string json) + { + const string cDisposition = "Content-Disposition: form-data; name=\"payload_json\"\r\n"; + const string cType = "Content-Type: application/octet-stream\r\n\r\n"; + AddField(stream, bound, cDisposition, cType, DiscordUtil.Encode(json)); + } + + private void SetFile(MemoryStream stream, string bound, int index, FileInfo file) + { + var cDisposition = $"Content-Disposition: form-data; name=\"file_{index}\"; filename=\"{file.Name}\"\r\n"; + const string cType = "Content-Type: application/octet-stream\r\n\r\n"; + AddField(stream, bound, cDisposition, cType, File.ReadAllBytes(file.FullName)); + } + + /// + /// Send webhook message + /// + public void Send(DiscordMessage message, params FileInfo[] files) + { + if (string.IsNullOrEmpty(Url)) + throw new ArgumentNullException($"Invalid Webhook URL {Url}."); + + var bound = "------------------------" + DateTime.Now.Ticks.ToString("x"); + var webhookRequest = new WebClient(); + webhookRequest.Headers.Add("Content-Type", "multipart/form-data; boundary=" + bound); + + var stream = new MemoryStream(); + for (var i = 0; i < files.Length; i++) + SetFile(stream, bound, i, files[i]); + + var json = message.ToString(); + SetJsonPayload(stream, bound, json); + + var bodyEnd = DiscordUtil.Encode($"\r\n--{bound}--"); + stream.Write(bodyEnd, 0, bodyEnd.Length); + + try + { + webhookRequest.UploadData(Url, stream.ToArray()); + } + catch (WebException ex) + { + throw new WebException(DiscordUtil.Decode(ex.Response.GetResponseStream())); + } + + stream.Dispose(); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/EventResult.cs b/GlobalTorch/API/EventResult.cs new file mode 100644 index 0000000..eea428e --- /dev/null +++ b/GlobalTorch/API/EventResult.cs @@ -0,0 +1,9 @@ +namespace Global.API +{ + public enum EventResult + { + Allow, + Continue, + Deny + } +} \ No newline at end of file diff --git a/GlobalTorch/API/GlobalModApi.cs b/GlobalTorch/API/GlobalModApi.cs new file mode 100644 index 0000000..7ad751a --- /dev/null +++ b/GlobalTorch/API/GlobalModApi.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Sandbox.ModAPI; +using VRage.Game.ModAPI; +using VRage.ObjectBuilders; + +namespace Global.API +{ + public class GlobalModApi + { + public static bool IsRunningGlobal() + { + return false; + } + + public static void GetAllBlocksOfType(List list, IMyCubeGrid grid, MyObjectBuilderType type) + { + list.AddRange(grid.GetFatBlocks().Where(block => block.BlockDefinition.TypeId == type)); + } + + public static void GetAllBlocksOfTypeId(List list, long gridId, MyObjectBuilderType type) + { + var grid = MyAPIGateway.Entities.GetEntityById(gridId) as IMyCubeGrid; + if (grid == null) return; + list.AddRange(grid.GetFatBlocks().Where(block => block.BlockDefinition.TypeId == type)); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/GlobalServerModApi.cs b/GlobalTorch/API/GlobalServerModApi.cs new file mode 100644 index 0000000..320b57d --- /dev/null +++ b/GlobalTorch/API/GlobalServerModApi.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Global.Shared.API; +using VRage.Game.ModAPI; +using VRage.ObjectBuilders; + +namespace Global.API +{ + public class GlobalServerModApi + { + public static bool IsRunningGlobal(ref bool __result) + { + __result = true; + return false; + } + + public static bool GetAllBlocksOfType(List list, IMyCubeGrid grid, MyObjectBuilderType type) + { + GetAllBlocksOfTypeId(list, grid.EntityId, type); + return false; + } + + public static bool GetAllBlocksOfTypeId(List list, long gridId, MyObjectBuilderType type) + { + list.AddRange(GlobalAPI.GetGridById(gridId).GetBlocks(type)); + return false; + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Libraries/SENetworkAPI/Client.cs b/GlobalTorch/API/Libraries/SENetworkAPI/Client.cs new file mode 100644 index 0000000..17f82a3 --- /dev/null +++ b/GlobalTorch/API/Libraries/SENetworkAPI/Client.cs @@ -0,0 +1,104 @@ +using System; +using Sandbox.ModAPI; +using VRage; +using VRage.Utils; +using VRageMath; + +namespace SENetworkAPI +{ + public class Client : NetworkAPI + { + /// + /// Handles communication with the server + /// + /// Identifies the channel to pass information to and from this mod + /// identifies what chat entries should be captured and sent to the server + public Client(ushort comId, string modName, string keyword = null) : base(comId, modName, keyword) + { + } + + /// + /// Sends a command packet to the server + /// + /// The command to be executed + /// Text that will be displayed in client chat + /// A serialized object to be sent across the network + /// The date timestamp this command was sent + /// The client reciving this packet (if 0 it sends to all clients) + /// Enture delivery of the packet + public override void SendCommand(string commandString, string message = null, byte[] data = null, + DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true) + { + if (MyAPIGateway.Session?.Player != null) + SendCommand( + new Command + { + CommandString = commandString, Message = message, Data = data, + Timestamp = sent == null ? DateTime.UtcNow.Ticks : sent.Value.Ticks, + SteamId = MyAPIGateway.Session.Player.SteamUserId + }, MyAPIGateway.Session.Player.SteamUserId, isReliable); + else + MyLog.Default.Warning($"[NetworkAPI] ComID: {ComId} | Failed to send command. Session does not exist."); + } + + /// + /// Sends a command packet to the server + /// + /// The object to be sent to the client + /// The users steam ID + /// Makes sure the message is recieved by the server + internal override void SendCommand(Command cmd, ulong steamId = ulong.MinValue, bool isReliable = true) + { + if (cmd.Data != null && cmd.Data.Length > CompressionThreshold) + { + cmd.Data = MyCompression.Compress(cmd.Data); + cmd.IsCompressed = true; + } + + cmd.Timestamp = DateTime.UtcNow.Ticks; + var packet = MyAPIGateway.Utilities.SerializeToBinary(cmd); + + if (LogNetworkTraffic) + MyLog.Default.Info( + $"[NetworkAPI] TRANSMITTING Bytes: {packet.Length} Command: {cmd.CommandString} User: {steamId}"); + + MyAPIGateway.Multiplayer.SendMessageToServer(ComId, packet, isReliable); + } + + /// + /// Sends a command packet to the server + /// + /// The command to be executed + /// Client side send to server this is not used + /// Client side send to server this is not used + /// Text that will be displayed in client chat + /// A serialized object to be sent across the network + /// The date timestamp this command was sent + /// The client reciving this packet (if 0 it sends to all clients) + /// Enture delivery of the packet + public override void SendCommand(string commandString, Vector3D point, double radius = 0, string message = null, + byte[] data = null, DateTime? sent = null, ulong steamId = 0, bool isReliable = true) + { + SendCommand(commandString, message, data, sent, steamId, isReliable); + } + + /// + /// Sends a command packet to the server + /// + /// The object to be sent to the client + /// Client side send to server this is not used + /// Client side send to server this is not used + /// The users steam ID + /// Makes sure the message is recieved by the server + internal override void SendCommand(Command cmd, Vector3D point, double radius = 0, ulong steamId = 0, + bool isReliable = true) + { + SendCommand(cmd, steamId, isReliable); + } + + public override void Say(string message) + { + SendCommand(null, message); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Libraries/SENetworkAPI/Command.cs b/GlobalTorch/API/Libraries/SENetworkAPI/Command.cs new file mode 100644 index 0000000..383e304 --- /dev/null +++ b/GlobalTorch/API/Libraries/SENetworkAPI/Command.cs @@ -0,0 +1,22 @@ +using ProtoBuf; + +namespace SENetworkAPI +{ + [ProtoContract] + internal class Command + { + [ProtoMember(1)] public ulong SteamId { get; set; } + + [ProtoMember(2)] public string CommandString { get; set; } + + [ProtoMember(3)] public string Message { get; set; } + + [ProtoMember(4)] public byte[] Data { get; set; } + + [ProtoMember(5)] public long Timestamp { get; set; } + + [ProtoMember(6)] public bool IsProperty { get; set; } + + [ProtoMember(7)] public bool IsCompressed { get; set; } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Libraries/SENetworkAPI/NetSync.cs b/GlobalTorch/API/Libraries/SENetworkAPI/NetSync.cs new file mode 100644 index 0000000..a9ef276 --- /dev/null +++ b/GlobalTorch/API/Libraries/SENetworkAPI/NetSync.cs @@ -0,0 +1,498 @@ +using System; +using System.Collections.Generic; +using ProtoBuf; +using Sandbox.Game.Entities; +using Sandbox.ModAPI; +using VRage.Game; +using VRage.Game.Components; +using VRage.Game.Entity; +using VRage.ModAPI; +using VRage.Utils; + +namespace SENetworkAPI +{ + public enum TransferType + { + ServerToClient, + ClientToServer, + Both + } + + public enum SyncType + { + Post, + Fetch, + Broadcast, + None + } + + [ProtoContract] + internal class SyncData + { + [ProtoMember(3)] public byte[] Data; + + [ProtoMember(2)] public long EntityId; + + [ProtoMember(1)] public long Id; + + [ProtoMember(4)] public SyncType SyncType; + } + + public abstract class NetSync + { + internal static Dictionary> PropertiesByEntity = + new Dictionary>(); + + internal static Dictionary PropertyById = new Dictionary(); + + internal static object locker = new object(); + internal static long generatorId = 1; + + /// + /// Triggers after recieving a fetch request from clients + /// and allows you to modify this property before it is sent. + /// + public Action BeforeFetchRequestResponse; + + /// + /// The allowed network communication direction + /// + public TransferType TransferType { get; internal set; } + + /// + /// The identity of this property + /// + public long Id { get; internal set; } + + /// + /// Enables/Disables network traffic out when setting a value + /// + public bool SyncOnLoad { get; internal set; } + + /// + /// Limits sync updates to within sync distance + /// + public bool LimitToSyncDistance { get; internal set; } + + /// + /// the last recorded network traffic + /// + public long LastMessageTimestamp { get; internal set; } + + internal static long GeneratePropertyId() + { + return generatorId++; + } + + /// + /// Request the lastest value from the server + /// + public abstract void Fetch(); + + internal abstract void Push(SyncType type, ulong sendTo); + + internal abstract void SetNetworkValue(byte[] data, ulong sender); + } + + public class NetSync : NetSync + { + private readonly string sessionName; + private T _value; + private MyEntity Entity; + + /// + /// Fires each time the value is changed + /// Provides the old value and the new value + /// + public Action ValueChanged; + + /// + /// Fires only when the a network call is made + /// Provides the old value and the new value + /// also provides the steamId + /// + public Action ValueChangedByNetwork; + + /// IMyEntity object this property is attached to + /// + /// Sets an initial value + /// automatically syncs data to clients when the class initializes + /// marking this true only sends data to clients within sync distance + public NetSync(IMyEntity entity, TransferType transferType, T startingValue = default, bool syncOnLoad = true, + bool limitToSyncDistance = true) + { + if (entity == null) + throw new Exception("[NetworkAPI] Attemped to create a NetSync property. MyEntity was null."); + + Init(entity as MyEntity, transferType, startingValue, syncOnLoad, limitToSyncDistance); + } + + /// MyEntity object this property is attached to + /// + /// Sets an initial value + /// automatically syncs data to clients when the class initializes + /// marking this true only sends data to clients within sync distance + public NetSync(MyEntity entity, TransferType transferType, T startingValue = default, bool syncOnLoad = true, + bool limitToSyncDistance = true) + { + if (entity == null) + throw new Exception("[NetworkAPI] Attemped to create a NetSync property. MyEntity was null."); + + Init(entity, transferType, startingValue, syncOnLoad, limitToSyncDistance); + } + + /// MyGameLogicComponent object this property is attached to + /// + /// Sets an initial value + /// automatically syncs data to clients when the class initializes + /// marking this true only sends data to clients within sync distance + public NetSync(MyGameLogicComponent logic, TransferType transferType, T startingValue = default, + bool syncOnLoad = true, bool limitToSyncDistance = true) + { + if (logic?.Entity == null) + throw new Exception( + "[NetworkAPI] Attemped to create a NetSync property. MyGameLogicComponent was null."); + + Init(logic.Entity as MyEntity, transferType, startingValue, syncOnLoad, limitToSyncDistance); + } + + /// MySessionComponentBase object this property is attached to + /// + /// Sets an initial value + /// automatically syncs data to clients when the class initializes + /// marking this true only sends data to clients within sync distance + public NetSync(MySessionComponentBase logic, TransferType transferType, T startingValue = default, + bool syncOnLoad = true, bool limitToSyncDistance = true) + { + if (logic == null) + throw new Exception( + "[NetworkAPI] Attemped to create a NetSync property. MySessionComponentBase was null."); + + sessionName = logic.GetType().Name; + Init(null, transferType, startingValue, syncOnLoad, limitToSyncDistance); + } + + /// + /// this property syncs across the network when changed + /// + public T Value + { + get => _value; + set => SetValue(value, SyncType.Broadcast); + } + + /// + /// This funtion is called by the constructer + /// + /// + /// Sets an initial value + /// automatically syncs data to clients when the class initializes + /// marking this true only sends data to clients within sync distance + private void Init(MyEntity entity, TransferType transferType, T startingValue = default, bool syncOnLoad = true, + bool limitToSyncDistance = true) + { + TransferType = transferType; + _value = startingValue; + SyncOnLoad = syncOnLoad; + LimitToSyncDistance = limitToSyncDistance; + + if (entity != null) + { + Entity = entity; + Entity.OnClose += Entity_OnClose; + + if (PropertiesByEntity.ContainsKey(Entity)) + { + PropertiesByEntity[Entity].Add(this); + Id = PropertiesByEntity[Entity].Count - 1; + } + else + { + PropertiesByEntity.Add(Entity, new List { this }); + Id = 0; + } + } + else + { + lock (locker) + { + Id = GeneratePropertyId(); + PropertyById.Add(Id, this); + } + } + + if (SyncOnLoad) + { + if (Entity != null) + Entity.AddedToScene += SyncOnAddedToScene; + else + Fetch(); + } + + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info( + $"[NetworkAPI] Property Created: {Descriptor()}, Transfer: {transferType}, SyncOnLoad: {SyncOnLoad}"); + } + + private void SyncOnAddedToScene(MyEntity e) + { + if (Entity != e) + return; + + Fetch(); + Entity.AddedToScene -= SyncOnAddedToScene; + } + + private void Entity_OnClose(MyEntity entity) + { + PropertyById.Remove(Id); + } + + /// + /// Allows you to change how syncing works when setting the value this way + /// + public void SetValue(T val, SyncType syncType = SyncType.None) + { + var oldval = _value; + lock (_value) + { + _value = val; + } + + SendValue(syncType); + ValueChanged?.Invoke(oldval, val); + } + + /// + /// Sets the data received over the network + /// + internal override void SetNetworkValue(byte[] data, ulong sender) + { + try + { + var oldval = _value; + lock (_value) + { + _value = MyAPIGateway.Utilities.SerializeFromBinary(data); + + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info($"[NetworkAPI] {Descriptor()} New value: {oldval} --- Old value: {_value}"); + } + + if (MyAPIGateway.Multiplayer.IsServer) SendValue(); + + ValueChanged?.Invoke(oldval, _value); + ValueChangedByNetwork?.Invoke(oldval, _value, sender); + } + catch (Exception e) + { + MyLog.Default.Error($"[NetworkAPI] Failed to deserialize network property data\n{e}"); + } + } + + /// + /// sends the value across the network + /// + private void SendValue(SyncType syncType = SyncType.Broadcast, ulong sendTo = ulong.MinValue) + { + try + { + if (!NetworkAPI.IsInitialized) + { + MyLog.Default.Error( + "[NetworkAPI] _ERROR_ The NetworkAPI has not been initialized. Use NetworkAPI.Init() to initialize it."); + return; + } + + if (syncType == SyncType.None) + { + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info($"[NetworkAPI] _INTERNAL_ {Descriptor()} Wont send value: {Value}"); + + return; + } + + if ((syncType != SyncType.Fetch && TransferType == TransferType.ServerToClient && + !MyAPIGateway.Multiplayer.IsServer) || + (TransferType == TransferType.ClientToServer && MyAPIGateway.Multiplayer.IsServer)) + { + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info( + $"[NetworkAPI] {Descriptor()} Bad send direction transfer type is {TransferType}"); + + return; + } + + if (MyAPIGateway.Session.OnlineMode == MyOnlineModeEnum.OFFLINE) + { + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info($"[NetworkAPI] _OFFLINE_ {Descriptor()} Wont send value: {Value}"); + + return; + } + + if (Value == null) + { + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Error( + $"[NetworkAPI] _ERROR_ {Descriptor()} Value is null. Cannot transmit null value."); + + return; + } + + var data = new SyncData + { + Id = Id, + EntityId = Entity != null ? Entity.EntityId : 0, + Data = MyAPIGateway.Utilities.SerializeToBinary(_value), + SyncType = syncType + }; + + var id = ulong.MinValue; + if (MyAPIGateway.Session?.LocalHumanPlayer != null) + id = MyAPIGateway.Session.LocalHumanPlayer.SteamUserId; + + if (id == sendTo && id != ulong.MinValue) + MyLog.Default.Error( + $"[NetworkAPI] _ERROR_ {Descriptor()} The sender id is the same as the recievers id. data will not be sent."); + + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info( + $"[NetworkAPI] _TRANSMITTING_ {Descriptor()} - Id:{data.Id}, EId:{data.EntityId}, {data.SyncType}, {(data.SyncType == SyncType.Fetch ? "" : $"Val:{_value}")}"); + + if (LimitToSyncDistance && Entity != null) + NetworkAPI.Instance.SendCommand( + new Command + { + IsProperty = true, Data = MyAPIGateway.Utilities.SerializeToBinary(data), SteamId = id + }, Entity.PositionComp.GetPosition(), steamId: sendTo); + else + NetworkAPI.Instance.SendCommand( + new Command + { + IsProperty = true, Data = MyAPIGateway.Utilities.SerializeToBinary(data), SteamId = id + }, sendTo); + } + catch (Exception e) + { + MyLog.Default.Error($"[NetworkAPI] _ERROR_ SendValue(): Problem syncing value: {e}"); + } + } + + /// + /// Receives and redirects all property traffic + /// + /// this hold the path to the property and the data to sync + internal static void RouteMessage(SyncData pack, ulong sender, long timestamp) + { + if (pack == null) + { + MyLog.Default.Error("[NetworkAPI] Property data is null"); + return; + } + + if (NetworkAPI.LogNetworkTraffic) + MyLog.Default.Info($"[NetworkAPI] Id:{pack.Id}, EId:{pack.EntityId}, {pack.SyncType}"); + + NetSync property; + if (pack.EntityId == 0) + { + if (!PropertyById.ContainsKey(pack.Id)) + { + MyLog.Default.Info("[NetworkAPI] id not registered in dictionary 'PropertyById'"); + return; + } + + property = PropertyById[pack.Id]; + } + else + { + var entity = (MyEntity)MyAPIGateway.Entities.GetEntityById(pack.EntityId); + + if (entity == null) + { + MyLog.Default.Info("[NetworkAPI] Failed to get entity by id"); + return; + } + + if (!PropertiesByEntity.ContainsKey(entity)) + { + MyLog.Default.Info("[NetworkAPI] Entity not registered in dictionary 'PropertiesByEntity'"); + return; + } + + var properties = PropertiesByEntity[entity]; + + if (pack.Id >= properties.Count) + { + MyLog.Default.Info("[NetworkAPI] property index out of range"); + return; + } + + property = properties[(int)pack.Id]; + } + + property.LastMessageTimestamp = timestamp; + if (pack.SyncType == SyncType.Fetch) + { + property.BeforeFetchRequestResponse?.Invoke(sender); + property.Push(SyncType.Post, sender); + } + else + { + property.SetNetworkValue(pack.Data, sender); + } + } + + /// + /// Request the lastest value from the server + /// Servers are not allowed to fetch from clients + /// + public override void Fetch() + { + if (!MyAPIGateway.Multiplayer.IsServer) SendValue(SyncType.Fetch); + } + + /// + /// Send data now + /// + public void Push() + { + SendValue(); + } + + /// + /// Send data to single user + /// + public void Push(ulong sendTo) + { + SendValue(SyncType.Post, sendTo); + } + + /// + /// Send data across the network now + /// + internal override void Push(SyncType type, ulong sendTo = ulong.MinValue) + { + SendValue(type, sendTo); + } + + /// + /// Identifier for logging readability + /// + internal string Descriptor() + { + if (Entity != null) + { + if (Entity is MyCubeBlock) + return + $"<{(Entity as MyCubeBlock).CubeGrid.DisplayName}_{(Entity.DefinitionId?.SubtypeId == null ? Entity.GetType().Name : Entity.DefinitionId?.SubtypeId.ToString())}.{Entity.EntityId}_{typeof(T).Name}.{Id}>"; + + return + $"<{(Entity.DefinitionId?.SubtypeId == null ? Entity.GetType().Name : Entity.DefinitionId?.SubtypeId.ToString())}.{Entity.EntityId}_{typeof(T).Name}.{Id}>"; + } + + return $"<{sessionName}_{typeof(T).Name}.{Id}>"; + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Libraries/SENetworkAPI/Network.cs b/GlobalTorch/API/Libraries/SENetworkAPI/Network.cs new file mode 100644 index 0000000..b0b9c65 --- /dev/null +++ b/GlobalTorch/API/Libraries/SENetworkAPI/Network.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Generic; +using Sandbox.ModAPI; +using VRage; +using VRage.Utils; +using VRageMath; + +namespace SENetworkAPI +{ + public enum NetworkTypes + { + Dedicated, + Server, + Client + } + + public abstract class NetworkAPI + { + public const int CompressionThreshold = 100000; + public static NetworkAPI Instance; + public static bool LogNetworkTraffic = false; + + /// + /// Gets the diffrence between now and a given timestamp in frames (60 fps) + /// + /// + /// + private static readonly double frames = 1000d / 60d; + + public readonly ushort ComId; + public readonly string Keyword; + public readonly string ModName; + internal Dictionary> ChatCommands = new Dictionary>(); + + internal Dictionary> NetworkCommands = + new Dictionary>(); + + /// + /// Event driven client, server syncing API. + /// + /// The communication channel this mod will listen on + /// The title use for displaying chat messages + /// The string identifying a chat command + public NetworkAPI(ushort comId, string modName, string keyword = null) + { + ComId = comId; + ModName = modName == null ? string.Empty : modName; + Keyword = keyword != null ? keyword.ToLower() : null; + + if (UsingTextCommands) + { + MyAPIGateway.Utilities.MessageEntered -= HandleChatInput; + MyAPIGateway.Utilities.MessageEntered += HandleChatInput; + } + + MyAPIGateway.Multiplayer.UnregisterMessageHandler(ComId, HandleIncomingPacket); + MyAPIGateway.Multiplayer.RegisterMessageHandler(ComId, HandleIncomingPacket); + + MyLog.Default.Info( + $"[NetworkAPI] Initialized. Type: {GetType().Name} ComId: {ComId} Name: {ModName} Keyword: {Keyword}"); + } + + public static bool IsInitialized => Instance != null; + + internal bool UsingTextCommands => Keyword != null; + + /// + /// Event triggers apon reciveing data over the network + /// steamId, command, data + /// + public event Action OnCommandRecived; + + /// + /// Invokes chat command events + /// + /// Chat message string + /// should be shown normally in global chat + private void HandleChatInput(string messageText, ref bool sendToOthers) + { + var args = messageText.ToLower().Split(' '); + if (args[0] != Keyword) + return; + sendToOthers = false; + + var arguments = messageText.Substring(Keyword.Length).Trim(' '); + + // Meh... this is kinda yucky + if (args.Length == 1 && ChatCommands.ContainsKey(string.Empty)) + { + ChatCommands[string.Empty]?.Invoke(string.Empty); + } + else if (args.Length > 1 && ChatCommands.ContainsKey(args[1])) + { + ChatCommands[args[1]]?.Invoke(arguments.Substring(args[1].Length).Trim(' ')); + } + else + { + if (!MyAPIGateway.Utilities.IsDedicated) + MyAPIGateway.Utilities.ShowMessage(ModName, "Command not recognized."); + } + } + + /// + /// Unpacks commands and handles arguments + /// + /// Data chunck recived from the network + private void HandleIncomingPacket(byte[] msg) + { + try + { + var cmd = MyAPIGateway.Utilities.SerializeFromBinary(msg); + + if (LogNetworkTraffic) + { + MyLog.Default.Info("[NetworkAPI] ----- TRANSMISSION RECIEVED -----"); + MyLog.Default.Info( + $"[NetworkAPI] Type: {(cmd.IsProperty ? "Property" : $"Command ID: {cmd.CommandString}")}, {(cmd.IsCompressed ? "Compressed, " : "")}From: {cmd.SteamId} "); + } + + if (cmd.IsCompressed) + { + cmd.Data = MyCompression.Decompress(cmd.Data); + cmd.IsCompressed = false; + } + + if (cmd.IsProperty) + { + NetSync.RouteMessage(MyAPIGateway.Utilities.SerializeFromBinary(cmd.Data), + cmd.SteamId, cmd.Timestamp); + } + else + { + if (!string.IsNullOrWhiteSpace(cmd.Message)) + { + if (!MyAPIGateway.Utilities.IsDedicated) + if (MyAPIGateway.Session != null) + MyAPIGateway.Utilities.ShowMessage(ModName, cmd.Message); + + if (MyAPIGateway.Multiplayer.IsServer) SendCommand(null, cmd.Message); + } + + if (cmd.CommandString != null) + { + OnCommandRecived?.Invoke(cmd.SteamId, cmd.CommandString, cmd.Data, new DateTime(cmd.Timestamp)); + + var command = cmd.CommandString.Split(' ')[0]; + + if (NetworkCommands.ContainsKey(command)) + NetworkCommands[command]?.Invoke(cmd.SteamId, cmd.CommandString, cmd.Data, + new DateTime(cmd.Timestamp)); + } + } + + if (LogNetworkTraffic) MyLog.Default.Info("[NetworkAPI] ----- END -----"); + } + catch (Exception e) + { + MyLog.Default.Error($"[NetworkAPI] Failure in message processing:\n{e}"); + } + } + + /// + /// Registers a callback that will fire when the command string is sent + /// + /// The command that triggers the callback + /// The function that runs when a command is recived + public void RegisterNetworkCommand(string command, Action callback) + { + if (command == null) + throw new Exception( + "[NetworkAPI] Cannot register a command using null. null is reserved for chat messages."); + + command = command.ToLower(); + + if (NetworkCommands.ContainsKey(command)) + throw new Exception( + $"[NetworkAPI] Failed to add the network command callback '{command}'. A command with the same name was already added."); + + NetworkCommands.Add(command, callback); + } + + /// + /// Unregisters a command + /// + /// + public void UnregisterNetworkCommand(string command) + { + if (NetworkCommands.ContainsKey(command)) NetworkCommands.Remove(command); + } + + /// + /// will trigger when you type + /// + /// + /// + /// this is the text command that will be typed into chat + /// this is the function that will be called when the keyword is typed + public void RegisterChatCommand(string command, Action callback) + { + if (command == null) command = string.Empty; + + command = command.ToLower(); + + if (ChatCommands.ContainsKey(command)) + throw new Exception( + $"[NetworkAPI] Failed to add the network command callback '{command}'. A command with the same name was already added."); + + ChatCommands.Add(command, callback); + } + + /// + /// Unregisters a chat command + /// + /// the chat command to unregister + public void UnregisterChatCommand(string command) + { + if (ChatCommands.ContainsKey(command)) ChatCommands.Remove(command); + } + + /// + /// Sends a command packet across the network + /// + /// The command word and any arguments delimidated with spaces + /// Text to be writen in chat + /// A serialized object used to send game information + /// The date timestamp this command was sent + /// A players steam id + /// Makes sure the data gets to the target + public abstract void SendCommand(string commandString, string message = null, byte[] data = null, + DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true); + + /// + /// Sends a command packet across the network + /// + /// The command word and any arguments delimidated with spaces + /// + /// + /// Text to be writen in chat + /// A serialized object used to send game information + /// The date timestamp this command was sent + /// A players steam id + /// Makes sure the data gets to the target + public abstract void SendCommand(string commandString, Vector3D point, double radius = 0, string message = null, + byte[] data = null, DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true); + + /// + /// Sends a command packet to the server / client + /// + /// The object to be sent across the network + /// the id of the user this is being sent to. 0 sends it to all users in range + /// make sure the packet reaches its destination + internal abstract void SendCommand(Command cmd, ulong steamId = ulong.MinValue, bool isReliable = true); + + + /// + /// Sends a command packet to the server / client if in range + /// + /// The object to be sent across the network + /// the center of the sending sphere + /// the radius of the sending sphere + /// the id of the user this is being sent to. 0 sends it to all users in range + /// make sure the packet reaches its destination + internal abstract void SendCommand(Command cmd, Vector3D point, double range = 0, + ulong steamId = ulong.MinValue, bool isReliable = true); + + /// + /// Posts text into the ingame chat. + /// + /// + public abstract void Say(string message); + + /// + /// Unregisters listeners + /// + [ObsoleteAttribute("This property is obsolete. Close is no longer required", false)] + public void Close() + { + MyLog.Default.Info($"[NetworkAPI] Unregistering communication stream: {ComId}"); + if (UsingTextCommands) MyAPIGateway.Utilities.MessageEntered -= HandleChatInput; + + MyAPIGateway.Multiplayer.UnregisterMessageHandler(ComId, HandleIncomingPacket); + } + + /// + /// Calls Instance.Close() + /// + [ObsoleteAttribute("This property is obsolete. Dispose is no longer required", false)] + public static void Dispose() + { + if (IsInitialized) Instance.Close(); + + Instance = null; + } + + /// + /// Initializes the default instance of the NetworkAPI + /// + public static void Init(ushort comId, string modName, string keyword = null) + { + if (IsInitialized) + return; + + if (!MyAPIGateway.Multiplayer.IsServer) + Instance = new Client(comId, modName, keyword); + else + Instance = new Server(comId, modName, keyword); + } + + /// + /// Gets the diffrence between now and a given timestamp in milliseconds + /// + /// + public static float GetDeltaMilliseconds(long timestamp) + { + return (DateTime.UtcNow.Ticks - timestamp) / TimeSpan.TicksPerMillisecond; + } + + public static int GetDeltaFrames(long timestamp) + { + return (int)Math.Ceiling(GetDeltaMilliseconds(timestamp) / frames); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Libraries/SENetworkAPI/Server.cs b/GlobalTorch/API/Libraries/SENetworkAPI/Server.cs new file mode 100644 index 0000000..a65aff5 --- /dev/null +++ b/GlobalTorch/API/Libraries/SENetworkAPI/Server.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using Sandbox.ModAPI; +using VRage; +using VRage.Game.ModAPI; +using VRage.Utils; +using VRageMath; + +namespace SENetworkAPI +{ + public class Server : NetworkAPI + { + /// + /// Server class contains a few server only feature beond what is inharited from the NetworkAPI + /// + /// Identifies the channel to pass information to and from this mod + /// identifies what chat entries should be captured and sent to the server + public Server(ushort comId, string modName, string keyword = null) : base(comId, modName, keyword) + { + } + + /// + /// Sends a command packet to the client(s) + /// + /// The command to be executed + /// Text that will be displayed in client chat + /// A serialized object to be sent across the network + /// The date timestamp this command was sent + /// The client reciving this packet (if 0 it sends to all clients) + /// Enture delivery of the packet + public override void SendCommand(string commandString, string message = null, byte[] data = null, + DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true) + { + SendCommand( + new Command + { + SteamId = steamId, CommandString = commandString, Message = message, Data = data, + Timestamp = sent == null ? DateTime.UtcNow.Ticks : sent.Value.Ticks + }, steamId, isReliable); + } + + /// + /// + /// Sends a command packet to the client(s) + /// the center of the sync location + /// the distance the message reaches (defaults to sync distance) + /// Text that will be displayed in client chat + /// A serialized object to be sent across the network + /// The date timestamp this command was sent + /// The client reciving this packet (if 0 it sends to all clients) + /// Enture delivery of the packet + public override void SendCommand(string commandString, Vector3D point, double radius = 0, string message = null, + byte[] data = null, DateTime? sent = null, ulong steamId = ulong.MinValue, bool isReliable = true) + { + SendCommand( + new Command + { + SteamId = steamId, CommandString = commandString, Message = message, Data = data, + Timestamp = sent == null ? DateTime.UtcNow.Ticks : sent.Value.Ticks + }, point, radius, steamId, isReliable); + } + + /// + /// Sends a command packet to a list of clients + /// + /// + /// The command to be executed + /// Text that will be displayed in client chat + /// A serialized object to be sent across the network + /// The date timestamp this command was sent + /// Enture delivery of the packet + public void SendCommandTo(ulong[] steamIds, string commandString, string message = null, byte[] data = null, + DateTime? sent = null, bool isReliable = true) + { + foreach (var id in steamIds) + SendCommand( + new Command + { + SteamId = id, CommandString = commandString, Message = message, Data = data, + Timestamp = sent == null ? DateTime.UtcNow.Ticks : sent.Value.Ticks + }, id, isReliable); + } + + /// + /// Sends a command packet to the client(s) + /// + /// The object to be sent to the client + /// The players steam id + /// Make sure the data arrives + internal override void SendCommand(Command cmd, ulong steamId = ulong.MinValue, bool isReliable = true) + { + if (cmd.Data != null && cmd.Data.Length > CompressionThreshold) + { + cmd.Data = MyCompression.Compress(cmd.Data); + cmd.IsCompressed = true; + } + + if (!string.IsNullOrWhiteSpace(cmd.Message) && MyAPIGateway.Multiplayer.IsServer && + MyAPIGateway.Session != null) MyAPIGateway.Utilities.ShowMessage(ModName, cmd.Message); + + var packet = MyAPIGateway.Utilities.SerializeToBinary(cmd); + + if (LogNetworkTraffic) + MyLog.Default.Info( + $"[NetworkAPI] TRANSMITTING Bytes: {packet.Length} Command: {cmd.CommandString} User: {steamId}"); + + if (steamId == ulong.MinValue) + MyAPIGateway.Multiplayer.SendMessageToOthers(ComId, packet, isReliable); + else + MyAPIGateway.Multiplayer.SendMessageTo(ComId, packet, steamId, isReliable); + } + + /// + /// Sends a command packet to the client(s) + /// + /// The object to be sent to the client + /// the center of the sync location + /// the distance the message reaches (defaults to sync distance) + /// The players steam id + /// Make sure the data arrives + internal override void SendCommand(Command cmd, Vector3D point, double radius = 0, + ulong steamId = ulong.MinValue, bool isReliable = true) + { + if (cmd.Data != null && cmd.Data.Length > CompressionThreshold) + { + cmd.Data = MyCompression.Compress(cmd.Data); + cmd.IsCompressed = true; + } + + if (radius == 0) radius = MyAPIGateway.Session.SessionSettings.SyncDistance; + + var players = new List(); + if (steamId == ulong.MinValue) + MyAPIGateway.Players.GetPlayers(players, + p => (p.GetPosition() - point).LengthSquared() < radius * radius && p.SteamUserId != cmd.SteamId); + else + MyAPIGateway.Players.GetPlayers(players, p => p.SteamUserId == steamId); + + if (!string.IsNullOrWhiteSpace(cmd.Message) && MyAPIGateway.Multiplayer.IsServer && + MyAPIGateway.Session != null) MyAPIGateway.Utilities.ShowMessage(ModName, cmd.Message); + + cmd.Timestamp = DateTime.UtcNow.Ticks; + var packet = MyAPIGateway.Utilities.SerializeToBinary(cmd); + + if (LogNetworkTraffic) + MyLog.Default.Info( + $"[NetworkAPI] _TRANSMITTING_ Bytes: {packet.Length} Command: {cmd.CommandString} To: {players.Count} Users within {radius}m"); + + foreach (var player in players) + MyAPIGateway.Multiplayer.SendMessageTo(ComId, packet, player.SteamUserId, isReliable); + } + + public override void Say(string message) + { + SendCommand(null, message); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Libraries/SENetworkAPI/SessionTools.cs b/GlobalTorch/API/Libraries/SENetworkAPI/SessionTools.cs new file mode 100644 index 0000000..c51a313 --- /dev/null +++ b/GlobalTorch/API/Libraries/SENetworkAPI/SessionTools.cs @@ -0,0 +1,13 @@ +using VRage.Game.Components; + +namespace SENetworkAPI +{ + [MySessionComponentDescriptor(MyUpdateOrder.NoUpdate)] + public class SessionTools : MySessionComponentBase + { + protected override void UnloadData() + { + NetworkAPI.Dispose(); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Util/CollectionUtils.cs b/GlobalTorch/API/Util/CollectionUtils.cs new file mode 100644 index 0000000..04932eb --- /dev/null +++ b/GlobalTorch/API/Util/CollectionUtils.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; + +namespace Global.API.Util +{ + public static class CollectionUtils + { + public static TV ComputeIfAbsent(this Dictionary self, TK key, Func valueCreator) + { + if (self.ContainsKey(key)) + return self[key]; + var val = valueCreator(key); + self.Add(key, val); + return val; + } + + public static void SetOrAdd(this Dictionary self, TK key, Func valueCreator, + Func valueIncrement) + { + if (self.ContainsKey(key)) + { + var current = self[key]; + self.Remove(key); + self.Add(key, valueIncrement(current)); + return; + } + + var val = valueCreator(key); + self.Add(key, val); + } + + public static bool TryGetElementAt(this IReadOnlyList self, int index, out T foundValue) + { + if (self.Count < index + 1) + { + foundValue = default; + return false; + } + + foundValue = self[index]; + return true; + } + + public static T GetElementAtIndexOrElse(this IReadOnlyList self, int index, T defaultValue) + { + return self.TryGetElementAt(index, out var e) ? e : defaultValue; + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Util/FacUtils.cs b/GlobalTorch/API/Util/FacUtils.cs new file mode 100644 index 0000000..af2fee9 --- /dev/null +++ b/GlobalTorch/API/Util/FacUtils.cs @@ -0,0 +1,45 @@ +using Sandbox.Game.Entities; +using Sandbox.Game.World; +using VRage.Game.ModAPI; + +namespace Global.API.Util +{ + public static class FacUtils + { + public static IMyFaction GetPlayersFaction(long playerId) + { + return MySession.Static.Factions.TryGetPlayerFaction(playerId); + } + + public static bool InSameFaction(long player1, long player2) + { + return GetPlayersFaction(player1) == GetPlayersFaction(player2); + } + + public static string GetFactionTag(long playerId) + { + var faction = MySession.Static.Factions.TryGetPlayerFaction(playerId); + + return faction == null ? "" : faction.Tag; + } + + public static long GetOwner(MyCubeGrid grid) + { + var ownersList = grid.BigOwners; + var totalOwners = ownersList.Count; + + if (totalOwners > 0 && ownersList[0] != 0) return ownersList[0]; + return totalOwners > 1 ? ownersList[1] : 0L; + } + + public static bool IsOwnerOrFactionOwned(MyCubeGrid grid, long playerId, bool doFactionCheck) + { + if (grid.BigOwners.Contains(playerId)) return true; + + if (!doFactionCheck) return false; + var ownerId = GetOwner(grid); + //check if the owner is a faction member, i honestly dont know the difference between grid.BigOwners and grid.SmallOwners + return InSameFaction(playerId, ownerId); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Util/FileUtils.cs b/GlobalTorch/API/Util/FileUtils.cs new file mode 100644 index 0000000..1a49e07 --- /dev/null +++ b/GlobalTorch/API/Util/FileUtils.cs @@ -0,0 +1,71 @@ +using System.IO; +using System.Xml.Serialization; +using Newtonsoft.Json; + +namespace Global.API.Util +{ + public static class FileUtils + { + public static void WriteToJsonFile(string filePath, T objectToWrite, bool append = false) where T : new() + { + TextWriter writer = null; + try + { + var contentsToWriteToFile = JsonConvert.SerializeObject(objectToWrite, Formatting.Indented); + writer = new StreamWriter(filePath, append); + writer.Write(contentsToWriteToFile); + } + finally + { + writer?.Close(); + } + } + + public static T ReadFromJsonFile(string filePath) where T : new() + { + TextReader reader = null; + try + { + reader = new StreamReader(filePath); + var fileContents = reader.ReadToEnd(); + return JsonConvert.DeserializeObject(fileContents); + } + finally + { + reader?.Close(); + } + } + + public static void WriteToXmlFile(string filePath, T objectToWrite, bool append = false) where T : new() + { + TextWriter writer = null; + try + { + var serializer = new XmlSerializer(typeof(T)); + writer = new StreamWriter(filePath, append); + serializer.Serialize(writer, objectToWrite); + } + finally + { + if (writer != null) + writer.Close(); + } + } + + public static T ReadFromXmlFile(string filePath) where T : new() + { + TextReader reader = null; + try + { + var serializer = new XmlSerializer(typeof(T)); + reader = new StreamReader(filePath); + return (T)serializer.Deserialize(reader); + } + finally + { + if (reader != null) + reader.Close(); + } + } + } +} \ No newline at end of file diff --git a/GlobalTorch/API/Util/GlobalLogManager.cs b/GlobalTorch/API/Util/GlobalLogManager.cs new file mode 100644 index 0000000..f67af07 --- /dev/null +++ b/GlobalTorch/API/Util/GlobalLogManager.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using Global.API.Util; +using Global.Shared.Logging; +using NLog; +using NLog.Config; +using NLog.Targets; + +namespace Global +{ + public static class GlobalLogManager + { + private static readonly Dictionary Targets = new Dictionary(); + + public static IPluginLogger GetLogger(string name) + { + return new TorchLogger(name); + } + + public static void PatchToUseCustomFileLogger(string name, string filename = null) + { + if (filename == null) filename = name; + var rules = LogManager.Configuration.LoggingRules; + for (var i = rules.Count - 1; i >= 0; i--) + if (rules[i].NameMatches(name)) + rules.RemoveAtFast(i); + + var logTarget = Targets.ComputeIfAbsent(filename, fn => new FileTarget(filename) + { + FileName = $"Logs/{fn}-latest.log", + Layout = "${var:logStamp} ${logger}: ${var:logContent}", + ArchiveOldFileOnStartup = true, + ArchiveNumbering = ArchiveNumberingMode.DateAndSequence, + ArchiveDateFormat = "dd-MM-yyyy", + ArchiveFileName = $"Logs/Archive/{fn}-{{###}}.zip", + EnableArchiveFileCompression = true + }); + var debugRule = new LoggingRule(name, LogLevel.Debug, logTarget); + rules.Insert(0, debugRule); + if (name == "Keen" && GlobalPlugin.LegacyConfig.LogKeenToConsole) + { + var consoleRule = new LoggingRule(name, LogLevel.Info, + LogManager.Configuration.FindTargetByName("console")); + consoleRule.Targets.Add(LogManager.Configuration.FindTargetByName("wpf")); + rules.Insert(0, consoleRule); + } + + LogManager.Configuration.Reload(); + LogManager.ReconfigExistingLoggers(); + } + + public static Logger GetCustomFileLogger(string name, string filename = null) + { + if (filename == null) filename = name; + var rules = LogManager.Configuration.LoggingRules; + var logTarget = Targets.ComputeIfAbsent(filename, fn => new FileTarget(filename) + { + FileName = $"Logs/{fn}-latest.log", + Layout = "${var:logStamp} ${logger}: ${var:logContent}", + ArchiveOldFileOnStartup = true, + ArchiveNumbering = ArchiveNumberingMode.DateAndSequence, + ArchiveDateFormat = "dd-MM-yyyy", + ArchiveFileName = $"Logs/Archive/{fn}-{{###}}.zip", + EnableArchiveFileCompression = true + }); + + var logRule = new LoggingRule(name, LogLevel.Debug, logTarget); + + rules.Insert(0, logRule); + + LogManager.Configuration.Reload(); + return LogManager.GetLogger(name); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/App.config b/GlobalTorch/App.config new file mode 100644 index 0000000..591889e --- /dev/null +++ b/GlobalTorch/App.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GlobalTorch/AudioSendManager.cs b/GlobalTorch/AudioSendManager.cs new file mode 100644 index 0000000..e75176d --- /dev/null +++ b/GlobalTorch/AudioSendManager.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using FragLabs.Audio.Codecs; +using FragLabs.Audio.Codecs.Opus; +using Global.Util; +using Sandbox.Game.VoiceChat; +using Torch.API; +using Torch.Managers; +using Torch.Utils; +using VRage.Library.Collections; +using VRage.Network; + +namespace Global +{ + public class AudioSendManager : Manager + { + [ReflectedMethodInfo(typeof(MyVoiceChatSessionComponent), "SendVoicePlayer")] + private static readonly MethodInfo _sendVoiceMethod; + + private readonly LegacyConfig _configData; + + private readonly object _lock = new object(); + + private readonly Dictionary _plays = new Dictionary(); + private readonly SendBuffer _sendBuffer = new SendBuffer(); + private readonly Thread _thread; + + private byte[] _buffer = Array.Empty(); + + private OpusEncoder _encoder; + + public AudioSendManager(ITorchBase torchInstance, LegacyConfig configData) : base(torchInstance) + { + _configData = configData; + _thread = new Thread(UpdateProc); + } + + public override void Attach() + { + base.Attach(); + _encoder = OpusEncoder.Create(24000, 1, Application.Restricted_LowLatency); + _thread.Start(); + } + + public override void Detach() + { + base.Detach(); + _encoder.Dispose(); + _thread.Join(1); + } + + private void UpdateProc() + { + while (true) + { + lock (_lock) + { + foreach (var (player, stream) in _plays) + { + // if stream can read and is not at the end + if (stream.CanRead && stream.Position < stream.Length) + { + ArrayExtensions.EnsureCapacity(ref _buffer, _configData.SendDataMs * 48); + var sampleLength = stream.Read(_buffer, 0, _configData.SendDataMs * 48); + if (sampleLength == 0) continue; + _sendBuffer.SenderUserId = (long)player; + _sendBuffer.VoiceDataBuffer = + _encoder.Encode(_buffer, sampleLength, out var encodedLength); + _sendBuffer.NumElements = encodedLength; + NetworkManager.RaiseStaticEvent(_sendVoiceMethod, 0UL, + _sendBuffer, + new EndpointId(player)); + } + else + { + stream.Dispose(); + _plays.Remove(player); + } + } + + Thread.Sleep(1); + } + } + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Config/WpfConfig.cs b/GlobalTorch/Config/WpfConfig.cs new file mode 100644 index 0000000..421582a --- /dev/null +++ b/GlobalTorch/Config/WpfConfig.cs @@ -0,0 +1,183 @@ +using System; +using System.Xml.Serialization; +using Global.Shared.Config; +using Global.Shared.Logging; +using Torch; +using Torch.Views; +using VRageMath; + +namespace Global.Config +{ + [Serializable] + public class WpfConfig : ViewModel, IPluginConfig + { + #region Base + + private bool _isEnabled = true; + + [Display(Name = "Enabled", Description = "Whether the plugin is enabled or not", GroupName = "Main")] + public bool IsEnabled + { + get => _isEnabled; + set => SetValue(ref _isEnabled, value); + } + + #endregion + + #region Database + + private string _host = "localhost"; + private int _port = 3306; + private string _database = "torch"; + private string _username = "root"; + private string _password = ""; + + [Display(Name = "Host", Description = "Address where database is located", GroupName = "Database", Order = 1)] + public string Host + { + get => _host; + set => SetValue(ref _host, value); + } + + [Display(Name = "Port", Description = "Port the database has access through", GroupName = "Database", + Order = 2)] + public int Port + { + get => _port; + set => SetValue(ref _port, value); + } + + [Display(Name = "Username", Description = "User to connect to the database with", GroupName = "Database", + Order = 3)] + public string Username + { + get => _username; + set => SetValue(ref _username, value); + } + + [Display(Name = "Password", Description = "Password for connection authentication", GroupName = "Database", + Order = 4)] + public string Password + { + get => _password; + set => SetValue(ref _password, value); + } + + [Display(Name = "Database", Description = "Which database you want to connect to", GroupName = "Database", + Order = 5)] + public string Database + { + get => _database; + set => SetValue(ref _database, value); + } + + #endregion + + #region Monitoring + + private bool _discordStatus; + private string _heartbeatUrl; + + [Display(Name = "Send Discord Status", + Description = "Whether to send a discord message when server is detected as crashed", + GroupName = "Monitoring")] + public bool SendDiscordStatus + { + get => _discordStatus; + set => SetValue(ref _discordStatus, value); + } + + [Display(Name = "Heartbeat URL", Description = "URL for heartbeat to be sent", GroupName = "Monitoring")] + public string HeartbeatUrl + { + get => _heartbeatUrl; + set => SetValue(ref _heartbeatUrl, value); + } + + #endregion + + #region Logging + + private LoggingLevel _level = LoggingLevel.Info; + + [Display(Name = "Logging Level", Description = "")] + public LoggingLevel Level + { + get => _level; + set => SetValue(ref _level, value); + } + + #endregion + + + #region OcTree + + private int _size = 40000000; + + [Display(Name = "Size", Description = "Size of the OcTree", GroupName = "OcTree", Order = 1)] + public int Size + { + get => _size; + set => SetValue(ref _size, value); + } + + private int _centerX = 0; + + [Display(Name = "Center X", Description = "X coordinate of the center of the OcTree", GroupName = "OcTree", Order = 2)] + public int CenterX + { + get => _centerX; + set => SetValue(ref _centerX, value); + } + + private int _centerY = 0; + + [Display(Name = "Center Y", Description = "Y coordinate of the center of the OcTree", GroupName = "OcTree", Order = 3)] + public int CenterY + { + get => _centerY; + set => SetValue(ref _centerY, value); + } + + private int _centerZ = 0; + + [Display(Name = "Center Z", Description = "Z coordinate of the center of the OcTree", GroupName = "OcTree", Order = 4)] + public int CenterZ + { + get => _centerZ; + set => SetValue(ref _centerZ, value); + } + + [XmlIgnore] + public Vector3I CenterPosition + { + get => new Vector3I(_centerX, _centerY, _centerZ); + set + { + SetValue(ref _centerX, value.X); + SetValue(ref _centerY, value.Y); + SetValue(ref _centerZ, value.Z); + } + } + + private int _capacity = 10; + + [Display(Name = "Capacity", Description = "Capacity of the OcTree", GroupName = "OcTree", Order = 5)] + public int Capacity + { + get => _capacity; + set => SetValue(ref _capacity, value); + } + + private int _maxDepth = 12; + + [Display(Name = "Max Depth", Description = "Max depth of the OcTree", GroupName = "OcTree", Order = 6)] + public int MaxDepth + { + get => _maxDepth; + set => SetValue(ref _maxDepth, value); + } + + #endregion + } +} \ No newline at end of file diff --git a/GlobalTorch/DBManager.cs b/GlobalTorch/DBManager.cs new file mode 100644 index 0000000..7215d03 --- /dev/null +++ b/GlobalTorch/DBManager.cs @@ -0,0 +1,16 @@ +namespace Global +{ + public class DbManager + { + public static string GetDbConnectionString() + { + return GlobalPlugin.WpfConfig == null + ? CreateConnectionString("mi3-ss47.a2hosting.com", 3306, "sigmadra_admin", "yW=ri?_=UnD^", "sigmadra_lexicon") + : CreateConnectionString(GlobalPlugin.WpfConfig.Host, GlobalPlugin.WpfConfig.Port, + GlobalPlugin.WpfConfig.Username, GlobalPlugin.WpfConfig.Password, GlobalPlugin.WpfConfig.Database); + } + + private static string CreateConnectionString(string server, int port, string username, string password, + string database) => $"server={server},{port};database={database};user={username};Password={password};"; + } +} \ No newline at end of file diff --git a/GlobalTorch/GlobalCommands.cs b/GlobalTorch/GlobalCommands.cs new file mode 100644 index 0000000..cc96897 --- /dev/null +++ b/GlobalTorch/GlobalCommands.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Global.API.Util; +using Sandbox.Game.Entities; +using Torch.Commands; +using Torch.Commands.Permissions; +using VRage.Game.ModAPI; + +namespace Global +{ + [Category("global")] + public class GlobalCommands : CommandModule + { + [Command("debug", "does thing.")] + [Permission(MyPromoteLevel.Admin)] + public void ReloadConfig() + { + Context.Respond($"There are {MyEntities.m_entityNameDictionary.Count} entities in the dictionary."); + var dictionary = new Dictionary(); + + foreach (var key in MyEntities.m_entityNameDictionary) + { + var type = key.Value.GetType(); + dictionary.SetOrAdd(type, t => 1, e => e + 1); + } + + foreach (var (k, v) in dictionary) Context.Respond($"{k.Name} : {v}"); + } + + [Command("crash")] + [Permission(MyPromoteLevel.Admin)] + public void Crash() + { + Context.Torch.Invoke(() => throw new Exception("This is a crash")); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/GlobalPlugin.cs b/GlobalTorch/GlobalPlugin.cs new file mode 100644 index 0000000..06e45e4 --- /dev/null +++ b/GlobalTorch/GlobalPlugin.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Windows.Controls; +using Global.API.Discord; +using Global.API.Util; +using Global.Config; +using Global.Patches; +using Global.Shared.Config; +using Global.Shared.Logging; +using Global.Shared.Patching; +using Global.Shared.Plugin; +using HarmonyLib; +using Sandbox.Game.GameSystems; +using SENetworkAPI; +using Torch; +using Torch.API; +using Torch.API.Managers; +using Torch.API.Plugins; +using Torch.Managers; +using Torch.Managers.PatchManager; +using Torch.Views; +using Timer = System.Timers.Timer; + +namespace Global +{ + public class GlobalPlugin : TorchPluginBase, IGlobalPlugin, IWpfPlugin + { + private const ushort ModId = 21031; + private const string ModName = "GlobalClientMod"; + public static Persistent PersistentConfig; + + private bool _init; + + public static LegacyConfig LegacyConfig { get; private set; } + public IPluginConfig Config => WpfConfig; + public static WpfConfig WpfConfig => PersistentConfig?.Data; + + public static AudioSendManager AudioManager { get; private set; } + + public long LastSent { get; private set; } = -1; + public bool SentDiscord { get; private set; } + public long Tick { get; private set; } + public bool Started { get; private set; } + public IPluginLogger Log { get; } = new TorchLogger("GlobalTorch"); + + public void Run(Action action) + { + Torch.Invoke(action); + } + + public UserControl GetControl() + { + return new PropertyGrid + { + DataContext = WpfConfig + }; + } + + public override void Init(ITorchBase torch) + { +#if DEBUG + // Allow the debugger some time to connect once the plugin assembly is loaded + Thread.Sleep(100); +#endif + base.Init(torch); + if (!File.Exists("opus.dll")) + using (var destination = File.Create("opus.dll")) + { + using (var manifestResourceStream = + typeof(GlobalPlugin).Assembly.GetManifestResourceStream("Global.opus.dll")) + { + if (manifestResourceStream != null) manifestResourceStream.CopyTo(destination); + else throw new Exception("Could not find opus.dll"); + } + } + + SetupConfig(); + if (LegacyConfig.OverwriteLogs) + { + // GlobalLogManager.PatchToUseCustomFileLogger("Keen"); + // GlobalLogManager.PatchToUseCustomFileLogger("Torch"); + } + + GlobalInstance.SetPlugin(this); + + // AudioManager = new AudioSendManager(Torch, Config); + // Torch.Managers.AddManager(AudioManager); + // Torch.Managers.AddManager(new ConsoleManager(Torch)); + + Patcher.InitilizePatcherContext(Torch.Managers.GetManager().AcquireContext()); + + if (!PatchHelpers.PatchAll(Log, new Harmony("Draconis.GlobalTorch"))) + { + Log.Error("Failed to patch all"); + return; + } + + MyScriptManagerPatch.ApplyPluginAPIPatch(); + + Log.Info("GlobalTorch Plugin Initialized"); + + if (LegacyConfig.EnableNexusIntegration) + { + var pluginManager = Torch.Managers.GetManager(); + if (pluginManager.Plugins.TryGetValue(NexusIntegration.NexusId, out _)) + { + Log.Info("Nexus Plugin Found, Loading Nexus Integration"); + NexusIntegration.Load(); + } + else + { + Log.Info("Nexus Plugin Not Found, Nexus Integration Not Loaded"); + } + } + + var timer = new Timer(15000); + timer.Elapsed += (sender, args) => + { + if (!Started || Torch.GameState > TorchGameState.Loaded) return; + + if (!string.IsNullOrWhiteSpace(WpfConfig.HeartbeatUrl)) + { + if (LastSent != Tick) + try + { + var request = WebRequest.Create(WpfConfig.HeartbeatUrl); + using (var res = request.GetResponse()) + { + } + } + catch (Exception e) + { + Log.Error(e, $"Error sending heartbeat to {WpfConfig.HeartbeatUrl}"); + } + else + Log.Error("Game crashed, not sending."); + } + + if (LastSent == Tick && WpfConfig.SendDiscordStatus && !SentDiscord) + try + { + var discordWebhook = new DiscordWebhook + { + Url = + "https://discord.com/api/webhooks/966330173303689297/9-71wWH7XQjXKVg92kFIaFQl8lFdtjsCWfsEWLQH2SeZP2Zc7tA_AbbovSGeN4asFCwZ" + }; + var discordMessage = new DiscordMessage + { + Content = $"<@&671423106102853642> {Torch.Config.InstanceName} HAS GONE DOWN!", + Username = Torch.Config.InstanceName, + AllowedMentions = new DiscordAllowedMentions + { + Parse = new List { "roles" } + } + }; + discordWebhook.Send(discordMessage); + SentDiscord = true; + } + catch (Exception e) + { + Log.Error(e, "Error sending discord status"); + } + + LastSent = Tick; + }; + timer.Enabled = true; + } + + public override void Update() + { + Tick++; + if (!_init) + { + GlobalStatic.Init(); + + // if (Config.EnableBoundingPatches) + // MyAPIGateway.Entities = new MyEntitiesButBetter(MyAPIGateway.Entities); + + _init = true; + + if (!NetworkAPI.IsInitialized) NetworkAPI.Init(ModId, ModName); + + Traverse.Create().Field("MAX_DISTANCE_RELATIVE_DAMPENING") + .SetValue(LegacyConfig.PatchesConfig.MaxDistanceRelativeDamping); + Traverse.Create().Field("MAX_DISTANCE_RELATIVE_DAMPENING_SQ").SetValue( + LegacyConfig.PatchesConfig.MaxDistanceRelativeDamping * + LegacyConfig.PatchesConfig.MaxDistanceRelativeDamping); + + Log.Info("GlobalTorch Plugin Initialized"); + + Started = true; + } + + GlobalStatic.Update(); + } + + public void SetupConfig() + { + var wpfConfigPath = Path.Combine(StoragePath, "Global2.cfg"); + PersistentConfig = Persistent.Load(wpfConfigPath); + + var configPath = $"{StoragePath}\\GlobalTorch.xml"; + LegacyConfig = File.Exists(configPath) + ? FileUtils.ReadFromXmlFile(configPath) + : new LegacyConfig(); + + FileUtils.WriteToXmlFile(configPath, LegacyConfig); + } + + public static bool IsLoggingLevel(LoggingLevel level) + { + if (WpfConfig == null) + return level.IsOfLevel(LoggingLevel.Info); + return WpfConfig.Level.IsOfLevel(level); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/GlobalTorch.csproj b/GlobalTorch/GlobalTorch.csproj new file mode 100644 index 0000000..1c56c5d --- /dev/null +++ b/GlobalTorch/GlobalTorch.csproj @@ -0,0 +1,273 @@ + + + + + Debug + AnyCPU + {BB44D53C-70F4-4EDD-9B03-321CDF62BCA4} + Library + Properties + Global + GlobalTorch + v4.8 + 512 + 7.3 + disable + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + if exist $(SolutionDir)TorchBinaries\Plugins\$(AssemblyName).zip ( Del $(SolutionDir)TorchBinaries\Plugins\$(AssemblyName).zip) + powershell.exe -command Compress-Archive -Path $(AssemblyName).dll, $(AssemblyName).pdb, 0Harmony.dll, RCONServerLib.dll, OpusWrapper.dll, manifest.xml -DestinationPath $(SolutionDir)TorchBinaries\Plugins\$(AssemblyName).zip + + + + + ..\..\NexusBinaries\0Harmony.dll + + + + False + ..\..\TorchBinaries\DedicatedServer64\Newtonsoft.Json.dll + False + + + ..\..\NexusBinaries\Nexus.dll + False + + + OpusWrapper.dll + + + + ..\..\TorchBinaries\DedicatedServer64\SpaceEngineers.Game.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\SpaceEngineers.ObjectBuilders.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\NLog.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\ProtoBuf.Net.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\ProtoBuf.Net.Core.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\Sandbox.Common.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\Sandbox.Game.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\Sandbox.Game.XmlSerializers.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\Sandbox.Graphics.dll + False + False + + + ..\..\TorchBinaries\DedicatedServer64\Sandbox.RenderDirect.dll + False + + + ..\..\TorchBinaries\DedicatedServer64\Steamworks.NET.dll + False + + + + + + + + + + ..\..\TorchBinaries\System.ComponentModel.Annotations.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\TorchBinaries\Torch.dll + + + ..\..\TorchBinaries\Torch.API.dll + + + ..\..\TorchBinaries\Torch.Server.exe + + + ..\..\TorchBinaries\DedicatedServer64\VRage.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Audio.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Dedicated.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.EOS.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Game.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Game.XmlSerializers.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Input.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Library.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Math.dll + + + ..\..\TorchBinaries\DedicatedServer64\VRage.Math.XmlSerializers.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + 1.6.0 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + 1.3.1 + + + 6.0.3 + + + 4.5.5 + + + 6.0.0 + + + + + + \ No newline at end of file diff --git a/GlobalTorch/GlobalTorch.csproj.user b/GlobalTorch/GlobalTorch.csproj.user new file mode 100644 index 0000000..1e3b11e --- /dev/null +++ b/GlobalTorch/GlobalTorch.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectFiles + + + F:\SteamLibrary\steamapps\common\SpaceEngineersDedicatedServer\DedicatedServer64\;C:\Program Files (x86)\Steam\steamapps\common\SpaceEngineers\Bin64\;C:\Users\Allen\Desktop\Stuff\SE Stuff\plugins\Nexus\;C:\Users\Allen\Desktop\Stuff\SE Stuff\Torch\ + + \ No newline at end of file diff --git a/GlobalTorch/LegacyConfig.cs b/GlobalTorch/LegacyConfig.cs new file mode 100644 index 0000000..cdce505 --- /dev/null +++ b/GlobalTorch/LegacyConfig.cs @@ -0,0 +1,52 @@ +using System.Xml.Serialization; +using VRageMath; + +namespace Global +{ + [XmlRoot("Config")] + public class LegacyConfig + { + public int BlockUpdateFrequency = 100; + public int Capacity = 8; + public bool EnableBoundingPatches = true; + + public bool EnableDebugLogging = false; + public bool EnableEntityNamePatch = true; + public bool EnableNexusIntegration = false; + public bool LogKeenToConsole = false; + public int MaxDepth = 10; + + public bool OverwriteLogs = false; + + [XmlElement("Patches")] public PatchesConfig PatchesConfig = new PatchesConfig(); + public int SendDataMs = 120; + + public Sync Sync = new Sync(); + public Vector3 WorldCenter = Vector3.Zero; + public Vector3 WorldSize = new Vector3(40000000, 40000000, 40000000); + public string InstanceName { get; set; } = "Unset Instance Name"; + public static LegacyConfig Instance => GlobalPlugin.LegacyConfig; + public static PatchesConfig Patches => Instance.PatchesConfig; + } + + public class PatchesConfig + { + public bool DisableAllPatches = false; + public bool EnableGridMerge = true; + public bool EnableModAPIPatch = true; + public float MaxDistanceRelativeDamping = 100f; + } + + public class Sync + { + public bool EnableSyncOverride = false; + + public float RadiusMultiplier = 4f; + public float SendFrequencyMultiplier = 2f; + + public int StartRadius = 20; + public int StartSendFrequency = 4; + public int StartUpdateFrequency = 60; + public float UpdateFrequencyMultiplier = 2f; + } +} \ No newline at end of file diff --git a/GlobalTorch/NexusIntegration.cs b/GlobalTorch/NexusIntegration.cs new file mode 100644 index 0000000..0ecae3d --- /dev/null +++ b/GlobalTorch/NexusIntegration.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nexus.API; +using VRageMath; + +namespace Global +{ + public static class NexusIntegration + { + public static Guid NexusId = new Guid("28a12184-0422-43ba-a6e6-2e228611cca5"); + public static bool EnableNexusIntegration; + + public static void Load() + { + EnableNexusIntegration = true; + } + + public static List GetSectors(BoundingSphereD sphereD) + { + if (!EnableNexusIntegration) return new List(); + return (from sector in NexusAPI.GetSectors() + let sectorSphere = new BoundingSphereD(sector.Center, sector.Radius) + where sectorSphere.Intersects(sphereD) + select sector).ToList(); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/OpusWrapper.dll b/GlobalTorch/OpusWrapper.dll new file mode 100644 index 0000000..0fcf990 Binary files /dev/null and b/GlobalTorch/OpusWrapper.dll differ diff --git a/GlobalTorch/Overwrites/MyEntitiesButBetter.cs b/GlobalTorch/Overwrites/MyEntitiesButBetter.cs new file mode 100644 index 0000000..79f2253 --- /dev/null +++ b/GlobalTorch/Overwrites/MyEntitiesButBetter.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using VRage.ModAPI; +using VRage.ObjectBuilders; +using VRageMath; + +namespace Global +{ + public class MyEntitiesButBetter : IMyEntities + { + private readonly List _entities = new List(); + private readonly List _entityIds = new List(); + private readonly IMyEntities m_entities; + + public MyEntitiesButBetter(IMyEntities mEntities) + { + m_entities = mEntities; + } + + public bool TryGetEntityById(long id, out IMyEntity entity) + { + return m_entities.TryGetEntityById(id, out entity); + } + + public bool TryGetEntityById(long? id, out IMyEntity entity) + { + return m_entities.TryGetEntityById(id, out entity); + } + + public bool TryGetEntityByName(string name, out IMyEntity entity) + { + return m_entities.TryGetEntityByName(name, out entity); + } + + public bool EntityExists(string name) + { + return m_entities.EntityExists(name); + } + + public void AddEntity(IMyEntity entity, bool insertIntoScene = true) + { + m_entities.AddEntity(entity, insertIntoScene); + } + + public IMyEntity CreateFromObjectBuilder(MyObjectBuilder_EntityBase objectBuilder) + { + return m_entities.CreateFromObjectBuilder(objectBuilder); + } + + public IMyEntity CreateFromObjectBuilderAndAdd(MyObjectBuilder_EntityBase objectBuilder) + { + return m_entities.CreateFromObjectBuilderAndAdd(objectBuilder); + } + + public void RemoveEntity(IMyEntity entity) + { + m_entities.RemoveEntity(entity); + } + + public bool IsSpherePenetrating(ref BoundingSphereD bs) + { + return m_entities.IsSpherePenetrating(ref bs); + } + + public Vector3D? FindFreePlace(Vector3D basePos, float radius, int maxTestCount = 20, int testsPerDistance = 5, + float stepSize = 1) + { + return m_entities.FindFreePlace(basePos, radius, maxTestCount, testsPerDistance, stepSize); + } + + public void GetInflatedPlayerBoundingBox(ref BoundingBox playerBox, float inflation) + { + m_entities.GetInflatedPlayerBoundingBox(ref playerBox, inflation); + } + + public void GetInflatedPlayerBoundingBox(ref BoundingBoxD playerBox, float inflation) + { + m_entities.GetInflatedPlayerBoundingBox(ref playerBox, inflation); + } + + public bool IsInsideVoxel(Vector3 pos, Vector3 hintPosition, out Vector3 lastOutsidePos) + { + return m_entities.IsInsideVoxel(pos, hintPosition, out lastOutsidePos); + } + + public bool IsInsideVoxel(Vector3D pos, Vector3D hintPosition, out Vector3D lastOutsidePos) + { + return m_entities.IsInsideVoxel(pos, hintPosition, out lastOutsidePos); + } + + public bool IsWorldLimited() + { + return m_entities.IsWorldLimited(); + } + + public float WorldHalfExtent() + { + return m_entities.WorldHalfExtent(); + } + + public float WorldSafeHalfExtent() + { + return m_entities.WorldSafeHalfExtent(); + } + + public bool IsInsideWorld(Vector3D pos) + { + return m_entities.IsInsideWorld(pos); + } + + public bool IsRaycastBlocked(Vector3D pos, Vector3D target) + { + return m_entities.IsRaycastBlocked(pos, target); + } + + public void SetEntityName(IMyEntity IMyEntity, bool possibleRename = true) + { + m_entities.SetEntityName(IMyEntity, possibleRename); + } + + public bool IsNameExists(IMyEntity entity, string name) + { + return m_entities.IsNameExists(entity, name); + } + + public void RemoveFromClosedEntities(IMyEntity entity) + { + m_entities.RemoveFromClosedEntities(entity); + } + + public void RemoveName(IMyEntity entity) + { + m_entities.RemoveName(entity); + } + + public bool Exist(IMyEntity entity) + { + return m_entities.Exist(entity); + } + + public void MarkForClose(IMyEntity entity) + { + m_entities.MarkForClose(entity); + } + + public void RegisterForUpdate(IMyEntity entity) + { + m_entities.RegisterForUpdate(entity); + } + + public void RegisterForDraw(IMyEntity entity) + { + m_entities.RegisterForDraw(entity); + } + + public void UnregisterForUpdate(IMyEntity entity, bool immediate = false) + { + m_entities.UnregisterForUpdate(entity, immediate); + } + + public void UnregisterForDraw(IMyEntity entity) + { + m_entities.UnregisterForDraw(entity); + } + + public IMyEntity GetIntersectionWithSphere(ref BoundingSphereD sphere) + { + return m_entities.GetIntersectionWithSphere(ref sphere); + } + + public IMyEntity GetIntersectionWithSphere(ref BoundingSphereD sphere, IMyEntity ignoreEntity0, + IMyEntity ignoreEntity1) + { + return m_entities.GetIntersectionWithSphere(ref sphere, ignoreEntity0, ignoreEntity1); + } + + public IMyEntity GetIntersectionWithSphere(ref BoundingSphereD sphere, IMyEntity ignoreEntity0, + IMyEntity ignoreEntity1, + bool ignoreVoxelMaps, bool volumetricTest, bool excludeEntitiesWithDisabledPhysics = false, + bool ignoreFloatingObjects = true, bool ignoreHandWeapons = true) + { + return m_entities.GetIntersectionWithSphere( + ref sphere, ignoreEntity0, ignoreEntity1, ignoreVoxelMaps, volumetricTest, + excludeEntitiesWithDisabledPhysics, ignoreFloatingObjects, ignoreHandWeapons); + } + + public IMyEntity GetEntityById(long entityId) + { + return m_entities.GetEntityById(entityId); + } + + public IMyEntity GetEntityById(long? entityId) + { + return m_entities.GetEntityById(entityId); + } + + public bool EntityExists(long entityId) + { + return m_entities.EntityExists(entityId); + } + + public bool EntityExists(long? entityId) + { + return m_entities.EntityExists(entityId); + } + + public IMyEntity GetEntityByName(string name) + { + return m_entities.GetEntityByName(name); + } + + public void SetTypeHidden(Type type, bool hidden) + { + m_entities.SetTypeHidden(type, hidden); + } + + public bool IsTypeHidden(Type type) + { + return m_entities.IsTypeHidden(type); + } + + public bool IsVisible(IMyEntity entity) + { + return m_entities.IsVisible(entity); + } + + public void UnhideAllTypes() + { + m_entities.UnhideAllTypes(); + } + + public void RemapObjectBuilderCollection(IEnumerable objectBuilders) + { + m_entities.RemapObjectBuilderCollection(objectBuilders); + } + + public void RemapObjectBuilder(MyObjectBuilder_EntityBase objectBuilder) + { + m_entities.RemapObjectBuilder(objectBuilder); + } + + public IMyEntity CreateFromObjectBuilderNoinit(MyObjectBuilder_EntityBase objectBuilder) + { + return m_entities.CreateFromObjectBuilderNoinit(objectBuilder); + } + + public void EnableEntityBoundingBoxDraw(IMyEntity entity, bool enable, Vector4? color = null, + float lineWidth = 0.01f, + Vector3? inflateAmount = null) + { + m_entities.EnableEntityBoundingBoxDraw(entity, enable, color, lineWidth, inflateAmount); + } + + public IMyEntity GetEntity(Func match) + { + return m_entities.GetEntity(match); + } + + public void GetEntities(HashSet entities, Func collect = null) + { + m_entities.GetEntities(entities, collect); + } + + public List GetIntersectionWithSphere(ref BoundingSphereD sphere, IMyEntity ignoreEntity0, + IMyEntity ignoreEntity1, + bool ignoreVoxelMaps, bool volumetricTest) + { + return m_entities.GetIntersectionWithSphere(ref sphere, + ignoreEntity0, ignoreEntity1, ignoreVoxelMaps, volumetricTest); + } + + public List GetEntitiesInAABB(ref BoundingBoxD boundingBox) + { + return m_entities.GetEntitiesInAABB(ref boundingBox); + } + + // public List GetEntitiesInSphere(ref BoundingSphereD boundingSphere) + // { + // var ents = MyGlobalGateway.OcTreeManager.GridTree.GetExact(_entityIds, boundingSphere); + // foreach (var ent in ents) _entities.Add(GetEntityById(ent)); + // ents.Clear(); + // return _entities; + // } + + public List GetElementsInBox(ref BoundingBoxD boundingBox) + { + return m_entities.GetElementsInBox(ref boundingBox); + } + + // public List GetTopMostEntitiesInSphere(ref BoundingSphereD boundingSphere) + // { + // var ents = MyGlobalGateway.OcTreeManager.GridTree.GetExact(_entityIds, boundingSphere); + // foreach (var ent in ents) _entities.Add(GetEntityById(ent)); + // ents.Clear(); + // ents = MyGlobalGateway.OcTreeManager.CharacterTree.GetExact(_entityIds, boundingSphere); + // foreach (var ent in ents) _entities.Add(GetEntityById(ent)); + // ents.Clear(); + // return _entities; + // } + // + // public List GetTopMostEntitiesInBox(ref BoundingBoxD boundingBox) + // { + // var ents = MyGlobalGateway.OcTreeManager.GridTree.GetExact(_entityIds, boundingBox); + // foreach (var ent in ents) _entities.Add(GetEntityById(ent)); + // ents.Clear(); + // ents = MyGlobalGateway.OcTreeManager.CharacterTree.GetExact(_entityIds, boundingBox); + // foreach (var ent in ents) _entities.Add(GetEntityById(ent)); + // ents.Clear(); + // return _entities; + // } + + //TODO: Replace with OcTree + public List GetEntitiesInSphere(ref BoundingSphereD boundingSphere) + { + return m_entities.GetEntitiesInSphere(ref boundingSphere); + } + + public List GetTopMostEntitiesInSphere(ref BoundingSphereD boundingSphere) + { + return m_entities.GetTopMostEntitiesInSphere(ref boundingSphere); + } + + public List GetTopMostEntitiesInBox(ref BoundingBoxD boundingBox) + { + return m_entities.GetTopMostEntitiesInBox(ref boundingBox); + } + + public IMyEntity CreateFromObjectBuilderParallel(MyObjectBuilder_EntityBase objectBuilder, + bool addToScene = false, + Action completionCallback = null) + { + return m_entities.CreateFromObjectBuilderParallel(objectBuilder, addToScene, completionCallback); + } + + event Action IMyEntities.OnEntityRemove + { + add => m_entities.OnEntityRemove += value; + remove => m_entities.OnEntityRemove -= value; + } + + event Action IMyEntities.OnEntityAdd + { + add => m_entities.OnEntityAdd += value; + remove => m_entities.OnEntityAdd -= value; + } + + event Action IMyEntities.OnCloseAll + { + add => m_entities.OnCloseAll += value; + remove => m_entities.OnCloseAll -= value; + } + + event Action IMyEntities.OnEntityNameSet + { + add => customEventDef += value; + remove => customEventDef -= value; + } + + private static event Action customEventDef; + + public static void CallRename(IMyEntity entity, string oldName, string newName) + { + customEventDef?.Invoke(entity, oldName, newName); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Patches/MyScriptManagerPatch.cs b/GlobalTorch/Patches/MyScriptManagerPatch.cs new file mode 100644 index 0000000..7d2eefd --- /dev/null +++ b/GlobalTorch/Patches/MyScriptManagerPatch.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; +using Global.API; +using Global.Shared.Plugin; +using HarmonyLib; +using Sandbox.Game.World; +using VRage.Game; +using VRage.Utils; + +namespace Global.Patches +{ + public class MyScriptManagerPatch + { + public static readonly Harmony Patcher = new Harmony("GlobalModApi"); + + public static void ApplyPluginAPIPatch() + { + Patches.Patcher.SuffixPatch("AddAssembly", + BindingFlags.Instance | BindingFlags.NonPublic, new[] + { + typeof(MyModContext), + typeof(MyStringId), + typeof(Assembly) + }, "CompileSuffix"); + } + + private static void CompileSuffix(MyModContext context, MyStringId myStringId, Assembly assembly) + { + if (!GlobalPlugin.LegacyConfig.PatchesConfig.EnableModAPIPatch) return; + var found = false; + foreach (var type in assembly.GetTypes()) + if (type.Name == "GlobalModApi") + { + GlobalInstance.Log.Info($"Found GlobalModApi in file: {type.Name}"); + PatchMethod(type, "IsRunningGlobal"); + PatchMethod(type, "GetAllBlocksOfType"); + PatchMethod(type, "GetAllBlocksOfTypeId"); + found = true; + } + } + + public static void PatchMethod(Type type, string methodName) + { + try + { + var method = type.GetMethod(methodName, + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (method != null) + { + Patcher.Patch(method, + new HarmonyMethod(GetAPIMethod(methodName))); + GlobalInstance.Log.Info($"Patched {methodName}"); + } + else + { + GlobalInstance.Log.Warning($"Could not find method {methodName} in type {type.FullName}"); + } + } + catch (Exception e) + { + GlobalInstance.Log.Error(e, $"Failed to patch method {methodName} in {type.FullName}"); + } + } + + public static MethodInfo GetAPIMethod(string v) + { + return typeof(GlobalServerModApi).GetMethod(v, + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Patches/Patcher.cs b/GlobalTorch/Patches/Patcher.cs new file mode 100644 index 0000000..891da50 --- /dev/null +++ b/GlobalTorch/Patches/Patcher.cs @@ -0,0 +1,131 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using NLog; +using Torch.Managers.PatchManager; + +namespace Global.Patches +{ + public static class Patcher + { + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + public static PatchContext ctx; + + + public static void InitilizePatcherContext(PatchContext Context) + { + ctx = Context; + } + + public static MethodInfo GetMethod(string TargetMethodName, BindingFlags Flags) + { + return MethodGet(typeof(T), TargetMethodName, Flags, null); + } + + public static MethodInfo GetMethod( + string TargetMethodName, + BindingFlags Flags, + Type[] Types) + { + return MethodGet(typeof(T), TargetMethodName, Flags, Types); + } + + public static MethodInfo SuffixPatch( + string TargetMethodName, + BindingFlags Flags, + string ReplaceMentMethodName) + { + return Patch(false, typeof(T), TargetMethodName, Flags, ReplaceMentMethodName, null); + } + + public static MethodInfo SuffixPatch( + string TargetMethodName, + BindingFlags Flags, + Type[] Types, + string ReplaceMentMethodName) + { + return Patch(false, typeof(T), TargetMethodName, Flags, ReplaceMentMethodName, Types); + } + + public static MethodInfo PrePatch( + string TargetMethodName, + BindingFlags Flags, + string ReplaceMentMethodName) + { + return Patch(true, typeof(T), TargetMethodName, Flags, ReplaceMentMethodName, null); + } + + public static MethodInfo PrePatch( + string TargetMethodName, + BindingFlags Flags, + Type[] Types, + string ReplaceMentMethodName) + { + return Patch(true, typeof(T), TargetMethodName, Flags, ReplaceMentMethodName, Types); + } + + private static MethodInfo Patch( + bool PreFix, + Type TargetClass, + string TargetMethodName, + BindingFlags Flags, + string ReplaceMentMethodName, + Type[] Types) + { + try + { + var method1 = Types != null + ? TargetClass.GetMethod(TargetMethodName, Flags, null, Types, null) + : TargetClass.GetMethod(TargetMethodName, Flags); + if (method1 == null) + { + Log.Error("Unable to find patch method " + TargetClass.Name + "." + TargetMethodName); + return null; + } + + var declaringType = new StackFrame(2, false).GetMethod().DeclaringType; + if (declaringType == null) + Log.Error("Unable to find calling class!"); + var method2 = declaringType.GetMethod(ReplaceMentMethodName, + BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (PreFix) + ctx.GetPattern(method1).Prefixes.Add(method2); + else + ctx.GetPattern(method1).Suffixes.Add(method2); + return method1; + } + catch (AmbiguousMatchException ex) + { + Log.Error(ex, + "You need to specify the method types! More than one method named: " + TargetClass.Name + "." + + TargetMethodName, Array.Empty()); + } + catch (ArgumentException ex) + { + Log.Error(ex, "Invalid Arguments for " + TargetMethodName + " exsisting in: " + TargetClass.Name, + Array.Empty()); + } + catch (Exception ex) + { + Log.Error(ex, "Unkown Patch Error!", Array.Empty()); + } + + return null; + } + + private static MethodInfo MethodGet( + Type TargetClass, + string TargetMethodName, + BindingFlags Flags, + Type[] Types) + { + var methodInfo = Types != null + ? TargetClass.GetMethod(TargetMethodName, Flags, null, Types, null) + : TargetClass.GetMethod(TargetMethodName, Flags); + if (!(methodInfo == null)) + return methodInfo; + Log.Error("Unable to find patch method " + TargetClass.Name + "." + TargetMethodName); + return null; + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Program.cs b/GlobalTorch/Program.cs new file mode 100644 index 0000000..c2fc18a --- /dev/null +++ b/GlobalTorch/Program.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Sandbox.ModAPI.Ingame; +using SpaceEngineers.Game.ModAPI.Ingame; + +namespace IngameScript +{ + public class Program : MyGridProgram + { + private const string InputMap = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#&^£%!$@[]{};:.,<>/|\\|!~\"'`()=+-*_"; + + private const string Password = "password"; + + public Program() + { + Runtime.UpdateFrequency = UpdateFrequency.Update1; + } + + public void Main(string argument, UpdateType updateType) + { + } + + private void Setup() + { + var keypad = GridTerminalSystem.GetBlockGroupWithName("Keypad"); + // get list of button panels + var buttons = new List(); + keypad.GetBlocksOfType(buttons); + + var maxCharacters = buttons.Count; + if (maxCharacters > InputMap.Length) + { + Echo("Too many buttons"); + return; + } + + for (var index = 0; index < buttons.Count; index++) + { + var myButtonPanel = buttons[index]; + myButtonPanel.SetCustomButtonName(0, InputMap[index].ToString()); + } + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Properties/AssemblyInfo.cs b/GlobalTorch/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b62c633 --- /dev/null +++ b/GlobalTorch/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GlobalTorch")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GlobalTorch")] +[assembly: AssemblyCopyright("Copyright © 2022")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("BB44D53C-70F4-4EDD-9B03-321CDF62BCA4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/GlobalTorch/TorchLogger.cs b/GlobalTorch/TorchLogger.cs new file mode 100644 index 0000000..5249092 --- /dev/null +++ b/GlobalTorch/TorchLogger.cs @@ -0,0 +1,114 @@ +using System; +using System.Runtime.CompilerServices; +using Global.Shared.Logging; +using NLog; + +namespace Global +{ + public class TorchLogger : LogFormatter, IPluginLogger + { + private readonly Logger _logger; + + public TorchLogger(string pluginName) : base("") + { + _logger = LogManager.GetLogger(pluginName); + } + + public bool IsTraceEnabled => _logger.IsTraceEnabled && GlobalPlugin.IsLoggingLevel(LoggingLevel.Trace); + public bool IsDebugEnabled => _logger.IsDebugEnabled && GlobalPlugin.IsLoggingLevel(LoggingLevel.Debug); + public bool IsInfoEnabled => _logger.IsInfoEnabled && GlobalPlugin.IsLoggingLevel(LoggingLevel.Info); + public bool IsWarningEnabled => _logger.IsWarnEnabled && GlobalPlugin.IsLoggingLevel(LoggingLevel.Warning); + public bool IsErrorEnabled => _logger.IsErrorEnabled && GlobalPlugin.IsLoggingLevel(LoggingLevel.Error); + public bool IsCriticalEnabled => _logger.IsFatalEnabled && GlobalPlugin.IsLoggingLevel(LoggingLevel.Fatal); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Trace(Exception ex, string message, params object[] data) + { + if (!IsTraceEnabled) + return; + + _logger.Trace(Format(ex, message, data)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Debug(Exception ex, string message, params object[] data) + { + if (!IsDebugEnabled) + return; + + _logger.Debug(Format(ex, message, data)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Info(Exception ex, string message, params object[] data) + { + if (!IsInfoEnabled) + return; + + _logger.Info(Format(ex, message, data)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Warning(Exception ex, string message, params object[] data) + { + if (!IsWarningEnabled) + return; + + _logger.Warn(Format(ex, message, data)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Error(Exception ex, string message, params object[] data) + { + if (!IsErrorEnabled) + return; + + _logger.Error(Format(ex, message, data)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Critical(Exception ex, string message, params object[] data) + { + if (!IsCriticalEnabled) + return; + + _logger.Fatal(Format(ex, message, data)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Trace(string message, params object[] data) + { + Trace(null, message, data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Debug(string message, params object[] data) + { + Debug(null, message, data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Info(string message, params object[] data) + { + Info(null, message, data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Warning(string message, params object[] data) + { + Warning(null, message, data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Error(string message, params object[] data) + { + Error(null, message, data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Critical(string message, params object[] data) + { + Critical(null, message, data); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Util/MTObservableCollection.cs b/GlobalTorch/Util/MTObservableCollection.cs new file mode 100644 index 0000000..9797cd1 --- /dev/null +++ b/GlobalTorch/Util/MTObservableCollection.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Windows.Threading; + +namespace Global.Util +{ + public class MtObservableCollection : ObservableCollection + { + public override event NotifyCollectionChangedEventHandler CollectionChanged; + + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + var collectionChanged = CollectionChanged; + if (collectionChanged == null) return; + foreach (var @delegate in collectionChanged.GetInvocationList()) + { + var nh = (NotifyCollectionChangedEventHandler)@delegate; + var dispObj = nh.Target as DispatcherObject; + var dispatcher = dispObj?.Dispatcher; + if (dispatcher != null && !dispatcher.CheckAccess()) + { + dispatcher.BeginInvoke( + (Action)(() => nh.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))), + DispatcherPriority.DataBind); + continue; + } + + nh.Invoke(this, e); + } + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Util/SEUtils.cs b/GlobalTorch/Util/SEUtils.cs new file mode 100644 index 0000000..d175371 --- /dev/null +++ b/GlobalTorch/Util/SEUtils.cs @@ -0,0 +1,60 @@ +using Sandbox.Game.World; + +namespace Global.Util +{ + public static class SEUtils + { + public static MyIdentity TryGetIdentity(string playerNameOrSteamId) + { + var player = MySession.Static.Players.GetPlayerByName(playerNameOrSteamId); + if (player != null) return player.Identity; + player = MySession.Static.Players.GetPlayerById(new MyPlayer.PlayerId(ulong.Parse(playerNameOrSteamId))); + if (player != null) return player.Identity; + if (MySession.Static.Players.TryGetPlayerBySteamId(ulong.Parse(playerNameOrSteamId), out player)) + return player.Identity; + + foreach (var identity in MySession.Static.Players.GetAllIdentities()) + { + if (identity.DisplayName == playerNameOrSteamId) return identity; + + if (!ulong.TryParse(playerNameOrSteamId, out var steamId)) continue; + + var id = MySession.Static.Players.TryGetSteamId(identity.IdentityId); + if (id == steamId) return identity; + if (identity.IdentityId == (long)steamId) return identity; + } + + return null; + } + + public static MyIdentity GetIdentityByNameOrId(string playerNameOrSteamId) + { + var player = MySession.Static.Players.GetPlayerByName(playerNameOrSteamId); + if (player != null) return player.Identity; + player = MySession.Static.Players.GetPlayerById(new MyPlayer.PlayerId(ulong.Parse(playerNameOrSteamId))); + if (player != null) return player.Identity; + + foreach (var identity in MySession.Static.Players.GetAllIdentities()) + { + if (identity.DisplayName == playerNameOrSteamId) return identity; + + if (!ulong.TryParse(playerNameOrSteamId, out var steamId)) continue; + + var id = MySession.Static.Players.TryGetSteamId(identity.IdentityId); + + if (id == steamId) return identity; + if (identity.IdentityId == (long)steamId) return identity; + } + + return null; + } + + public static string GetPlayerName(ulong steamId) + { + var id = GetIdentityByNameOrId(steamId.ToString()); + if (id != null && id.DisplayName != null) return id.DisplayName; + + return steamId.ToString(); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/Util/SendBuffer.cs b/GlobalTorch/Util/SendBuffer.cs new file mode 100644 index 0000000..5ad28f6 --- /dev/null +++ b/GlobalTorch/Util/SendBuffer.cs @@ -0,0 +1,36 @@ +using System; +using VRage.Library.Collections; + +namespace Global.Util +{ + internal class SendBuffer : IBitSerializable + { + public int NumElements; + public long SenderUserId; + public byte[] VoiceDataBuffer; + + public bool Serialize(BitStream stream, bool validate, bool acceptAndSetValue = true) + { + if (stream.Reading) + { + SenderUserId = stream.ReadInt64(); + NumElements = stream.ReadInt32(); + ArrayExtensions.EnsureCapacity(ref VoiceDataBuffer, NumElements); + stream.ReadBytes(VoiceDataBuffer, 0, NumElements); + } + else + { + stream.WriteInt64(SenderUserId); + stream.WriteInt32(NumElements); + stream.WriteBytes(VoiceDataBuffer, 0, NumElements); + } + + return true; + } + + public static implicit operator BitReaderWriter(SendBuffer buffer) + { + return new BitReaderWriter(buffer); + } + } +} \ No newline at end of file diff --git a/GlobalTorch/manifest.xml b/GlobalTorch/manifest.xml new file mode 100644 index 0000000..b95ca74 --- /dev/null +++ b/GlobalTorch/manifest.xml @@ -0,0 +1,7 @@ + + + GlobalTorch + FB668D21-68CE-4037-9564-807E2709836C + None + 0.1.0 + \ No newline at end of file diff --git a/GlobalTorch/opus.dll b/GlobalTorch/opus.dll new file mode 100644 index 0000000..4265850 Binary files /dev/null and b/GlobalTorch/opus.dll differ