diff --git a/Advanced Gravity Collector/Data/AssemblerBlueprint.sbc b/Advanced Gravity Collector/Data/AssemblerBlueprint.sbc
new file mode 100644
index 00000000..bb63ba5e
--- /dev/null
+++ b/Advanced Gravity Collector/Data/AssemblerBlueprint.sbc
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/Blocks.sbc b/Advanced Gravity Collector/Data/Blocks.sbc
new file mode 100644
index 00000000..8ff4eb16
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Blocks.sbc
@@ -0,0 +1,104 @@
+
+
+
+
+
+ Collector
+ LargeGravityCollector
+
+ Gravity Collector
+ Textures\Icons\GravityCollector.dds
+ Large
+ TriangleMesh
+
+
+ Models\LargeGravityCollector.mwm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ GravityCollector
+ Light
+ 28
+ 0.6
+ Conveyors
+
+ 2.5
+ 2.5
+ 1
+
+
+
+
+
+
+
+
+ Z
+ Y
+ 212
+
+
+
+ Collector
+ MediumGravityCollector
+
+ Gravity Collector
+ Textures\Icons\GravityCollector.dds
+ Small
+ TriangleMesh
+
+
+ Models\MediumGravityCollector.mwm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ GravityCollector
+ Light
+ 18
+ 0.3
+ Conveyors
+
+ 1.5
+ 1.5
+ 0.7
+
+
+
+
+
+
+
+
+ Z
+ Y
+ 212
+
+
+
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/Categories.sbc b/Advanced Gravity Collector/Data/Categories.sbc
new file mode 100644
index 00000000..2ba1e2b0
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Categories.sbc
@@ -0,0 +1,38 @@
+
+
+
+
+
+ GuiBlockCategoryDefinition
+
+
+ DisplayName_Category_LargeBlocks
+ LargeBlocks
+
+ Collector/LargeGravityCollector
+
+
+
+
+ GuiBlockCategoryDefinition
+
+
+ DisplayName_Category_SmallBlocks
+ SmallBlocks
+
+ Collector/MediumGravityCollector
+
+
+
+
+ GuiBlockCategoryDefinition
+
+
+ DisplayName_Category_ConveyorBlocks
+ Conveyors
+
+ GravityCollector
+
+
+
+
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/Categories_Digi.sbc b/Advanced Gravity Collector/Data/Categories_Digi.sbc
new file mode 100644
index 00000000..4dacf837
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Categories_Digi.sbc
@@ -0,0 +1,16 @@
+
+
+
+
+
+ GuiBlockCategoryDefinition
+
+
+ + Digi
+ z-mod-digi
+
+ GravityCollector
+
+
+
+
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/EntityComponents.sbc b/Advanced Gravity Collector/Data/EntityComponents.sbc
new file mode 100644
index 00000000..b773db23
--- /dev/null
+++ b/Advanced Gravity Collector/Data/EntityComponents.sbc
@@ -0,0 +1,13 @@
+
+
+
+
+ ModStorageComponent
+ GravityCollector
+
+
+ 0DFC6F70-310D-4D1C-A55F-C57913E20389
+
+
+
+
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollector.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollector.cs
new file mode 100644
index 00000000..c7744da7
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollector.cs
@@ -0,0 +1,666 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Sandbox.Common.ObjectBuilders;
+using Sandbox.Definitions;
+using Sandbox.Game.Entities;
+using Sandbox.Game.EntityComponents;
+using Sandbox.ModAPI;
+using Sandbox.ModAPI.Interfaces.Terminal;
+using VRage.Game;
+using VRage.Game.Components;
+using VRage.Game.ModAPI;
+using VRage.ModAPI;
+using VRage.ObjectBuilders;
+using VRage.Utils;
+using VRageMath;
+
+namespace Digi.GravityCollector
+{
+ [MyEntityComponentDescriptor(typeof(MyObjectBuilder_Collector), false, "MediumGravityCollector", "LargeGravityCollector")]
+ public class GravityCollector : MyGameLogicComponent
+ {
+ public const float RANGE_MIN = 0;
+ public const float RANGE_MAX_MEDIUM = 40;
+ public const float RANGE_MAX_LARGE = 60;
+ public const float RANGE_OFF_EXCLUSIVE = 1;
+
+ public const float STRENGTH_MIN = 1;
+ public const float STRENGTH_MAX = 200;
+
+ public const int APPLY_FORCE_SKIP_TICKS = 3; // how many ticks between applying forces to floating objects
+ public const double MAX_VIEW_RANGE_SQ = 500 * 500; // max distance that the cone and pulsing item sprites can be seen from, squared value.
+
+ public const float MASS_MUL = 10; // multiply item mass to get force
+ public const float MAX_MASS = 5000; // max mass to multiply
+
+ public const string CONTROLS_PREFIX = "GravityCollector.";
+ public readonly Guid SETTINGS_GUID = new Guid("0DFC6F70-310D-4D1C-A55F-C57913E20389");
+ public const int SETTINGS_CHANGED_COUNTDOWN = (60 * 1) / 10; // div by 10 because it runs in update10
+
+ public float Range
+ {
+ get { return Settings.Range; }
+ set
+ {
+ Settings.Range = MathHelper.Clamp((int)Math.Floor(value), RANGE_MIN, maxRange);
+
+ SettingsChanged();
+
+ if(Settings.Range < RANGE_OFF_EXCLUSIVE)
+ {
+ NeedsUpdate = MyEntityUpdateEnum.NONE;
+ }
+ else
+ {
+ if((NeedsUpdate & MyEntityUpdateEnum.EACH_10TH_FRAME) == 0)
+ NeedsUpdate |= MyEntityUpdateEnum.EACH_10TH_FRAME;
+ }
+
+ block?.Components?.Get()?.Update();
+ }
+ }
+
+ public float StrengthMul
+ {
+ get { return Settings.Strength; }
+ set
+ {
+ Settings.Strength = MathHelper.Clamp(value, STRENGTH_MIN / 100f, STRENGTH_MAX / 100f);
+
+ SettingsChanged();
+
+ block?.Components?.Get()?.Update();
+ }
+ }
+
+ IMyCollector block;
+ MyPoweredCargoContainerDefinition blockDef;
+
+ public readonly GravityCollectorBlockSettings Settings = new GravityCollectorBlockSettings();
+ int syncCountdown;
+
+ double coneAngle;
+ float offset;
+ float maxRange;
+
+ int skipTicks;
+ List floatingObjects;
+
+ GravityCollectorMod Mod => GravityCollectorMod.Instance;
+
+ bool DrawCone
+ {
+ get
+ {
+ if(MyAPIGateway.Utilities.IsDedicated || !block.ShowOnHUD)
+ return false;
+
+ var relation = block.GetPlayerRelationToOwner();
+
+ return (relation != MyRelationsBetweenPlayerAndBlock.Enemies);
+ }
+ }
+
+ public override void Init(MyObjectBuilder_EntityBase objectBuilder)
+ {
+ NeedsUpdate = MyEntityUpdateEnum.BEFORE_NEXT_FRAME;
+ }
+
+ public override void UpdateOnceBeforeFrame()
+ {
+ try
+ {
+ SetupTerminalControls();
+
+ block = (IMyCollector)Entity;
+
+ if(block.CubeGrid?.Physics == null)
+ return;
+
+ blockDef = (MyPoweredCargoContainerDefinition)block.SlimBlock.BlockDefinition;
+
+ floatingObjects = new List();
+
+ switch(block.BlockDefinition.SubtypeId)
+ {
+ case "MediumGravityCollector":
+ maxRange = RANGE_MAX_MEDIUM;
+ coneAngle = MathHelper.ToRadians(30);
+ offset = 0.75f;
+ break;
+ case "LargeGravityCollector":
+ maxRange = RANGE_MAX_LARGE;
+ coneAngle = MathHelper.ToRadians(25);
+ offset = 1.5f;
+ break;
+ }
+
+ NeedsUpdate = MyEntityUpdateEnum.EACH_FRAME | MyEntityUpdateEnum.EACH_10TH_FRAME;
+
+ // override block's power usage behavior
+ var sink = block.Components?.Get();
+ sink?.SetRequiredInputFuncByType(MyResourceDistributorComponent.ElectricityId, ComputeRequiredPower);
+
+ // set default settings
+ Settings.Strength = 1.0f;
+ Settings.Range = maxRange;
+
+ if(!LoadSettings())
+ {
+ ParseLegacyNameStorage();
+ }
+
+ SaveSettings(); // required for IsSerialized()
+ }
+ catch(Exception e)
+ {
+ Log.Error(e);
+ }
+ }
+
+ public override void Close()
+ {
+ try
+ {
+ if(block == null)
+ return;
+
+ floatingObjects?.Clear();
+ floatingObjects = null;
+
+ block = null;
+ }
+ catch(Exception e)
+ {
+ Log.Error(e);
+ }
+ }
+
+ float ComputeRequiredPower()
+ {
+ if(!block.IsWorking)
+ return 0f;
+
+ var baseUsage = 0.002f; // same as vanilla collector
+ var maxPowerUsage = blockDef.RequiredPowerInput;
+ var mul = (StrengthMul / (STRENGTH_MAX / 100f)) * (Range / maxRange);
+ return baseUsage + maxPowerUsage * mul;
+ }
+
+ public override void UpdateBeforeSimulation10()
+ {
+ try
+ {
+ SyncSettings();
+ FindFloatingObjects();
+ }
+ catch(Exception e)
+ {
+ Log.Error(e);
+ }
+ }
+
+ void FindFloatingObjects()
+ {
+ var entities = Mod.Entities;
+ entities.Clear();
+ floatingObjects.Clear();
+
+ if(Range < RANGE_OFF_EXCLUSIVE || !block.IsWorking || !block.CubeGrid.Physics.Enabled)
+ {
+ if((NeedsUpdate & MyEntityUpdateEnum.EACH_FRAME) != 0)
+ {
+ UpdateEmissive();
+ NeedsUpdate &= ~MyEntityUpdateEnum.EACH_FRAME;
+ }
+
+ return;
+ }
+
+ if((NeedsUpdate & MyEntityUpdateEnum.EACH_FRAME) == 0)
+ NeedsUpdate |= MyEntityUpdateEnum.EACH_FRAME;
+
+ var collectPos = block.WorldMatrix.Translation + (block.WorldMatrix.Forward * offset);
+ var sphere = new BoundingSphereD(collectPos, Range + 10);
+
+ MyGamePruningStructure.GetAllTopMostEntitiesInSphere(ref sphere, entities, MyEntityQueryType.Dynamic);
+
+ foreach(var ent in entities)
+ {
+ var floatingObject = ent as IMyFloatingObject;
+
+ if(floatingObject != null && floatingObject.Physics != null)
+ floatingObjects.Add(floatingObject);
+ }
+
+ entities.Clear();
+ }
+
+ private Color prevColor;
+
+ void UpdateEmissive(bool pulling = false)
+ {
+ var color = Color.Red;
+ float strength = 0f;
+
+ if(block.IsWorking)
+ {
+ strength = 1f;
+
+ if(pulling)
+ color = Color.Cyan;
+ else
+ color = new Color(10, 255, 0);
+ }
+
+ if(prevColor == color)
+ return;
+
+ prevColor = color;
+ block.SetEmissiveParts("Emissive", color, strength);
+ }
+
+ public override void UpdateAfterSimulation()
+ {
+ try
+ {
+ if(Range < RANGE_OFF_EXCLUSIVE)
+ return;
+
+ bool applyForce = false;
+ if(++skipTicks >= APPLY_FORCE_SKIP_TICKS)
+ {
+ skipTicks = 0;
+ applyForce = true;
+ }
+
+ if(!applyForce && MyAPIGateway.Utilities.IsDedicated)
+ return;
+
+ var conePos = block.WorldMatrix.Translation + (block.WorldMatrix.Forward * -offset);
+ bool inViewRange = false;
+
+ if(!MyAPIGateway.Utilities.IsDedicated)
+ {
+ var cameraMatrix = MyAPIGateway.Session.Camera.WorldMatrix;
+ inViewRange = Vector3D.DistanceSquared(cameraMatrix.Translation, conePos) <= MAX_VIEW_RANGE_SQ;
+
+ if(inViewRange && DrawCone)
+ DrawInfluenceCone(conePos);
+ }
+
+ if(!applyForce && !inViewRange)
+ return;
+
+ if(floatingObjects.Count == 0)
+ {
+ UpdateEmissive();
+ return;
+ }
+
+ var collectPos = block.WorldMatrix.Translation + (block.WorldMatrix.Forward * offset);
+ var blockVel = block.CubeGrid.Physics.GetVelocityAtPoint(collectPos);
+ var rangeSq = Range * Range;
+ int pulling = 0;
+
+ for(int i = (floatingObjects.Count - 1); i >= 0; --i)
+ {
+ var floatingObject = floatingObjects[i];
+
+ if(floatingObject.MarkedForClose || !floatingObject.Physics.Enabled)
+ continue; // it'll get removed by FindFloatingObjects()
+
+ var objPos = floatingObject.GetPosition();
+ var distSq = Vector3D.DistanceSquared(collectPos, objPos);
+
+ if(distSq > rangeSq)
+ continue; // too far from cone
+
+ var dirNormalized = Vector3D.Normalize(objPos - conePos);
+ var angle = Math.Acos(MathHelper.Clamp(Vector3D.Dot(block.WorldMatrix.Forward, dirNormalized), -1, 1));
+
+ if(angle > coneAngle)
+ continue; // outside of the cone's FOV
+
+ if(applyForce)
+ {
+ var collectDir = Vector3D.Normalize(objPos - collectPos);
+
+ var vel = floatingObject.Physics.LinearVelocity - blockVel;
+ var stop = vel - (collectDir * collectDir.Dot(vel));
+ var force = -(stop + collectDir) * Math.Min(floatingObject.Physics.Mass * MASS_MUL, MAX_MASS) * StrengthMul;
+
+ force *= APPLY_FORCE_SKIP_TICKS; // multiplied by how many ticks were skipped
+
+ //MyTransparentGeometry.AddLineBillboard(Mod.MATERIAL_SQUARE, Color.Yellow, objPos, force, 1f, 0.1f);
+
+ floatingObject.Physics.AddForce(MyPhysicsForceType.APPLY_WORLD_FORCE, force, null, null);
+ }
+
+ if(inViewRange)
+ {
+ var mul = (float)Math.Sin(DateTime.UtcNow.TimeOfDay.TotalMilliseconds * 0.01);
+ var radius = floatingObject.Model.BoundingSphere.Radius * MinMaxPercent(0.75f, 1.25f, mul);
+
+ MyTransparentGeometry.AddPointBillboard(Mod.MATERIAL_DOT, Color.LightSkyBlue * MinMaxPercent(0.2f, 0.4f, mul), objPos, radius, 0);
+ }
+
+ pulling++;
+ }
+
+ if(applyForce)
+ UpdateEmissive(pulling > 0);
+ }
+ catch(Exception e)
+ {
+ Log.Error(e);
+ }
+ }
+
+ void DrawInfluenceCone(Vector3D conePos)
+ {
+ Vector4 color = Color.Cyan.ToVector4() * 10;
+ Vector4 planeColor = (Color.White * 0.1f).ToVector4();
+ const float LINE_THICK = 0.02f;
+ const int WIRE_DIV_RATIO = 16;
+
+ var coneMatrix = block.WorldMatrix;
+ coneMatrix.Translation = conePos;
+
+ //MyTransparentGeometry.AddPointBillboard(Mod.MATERIAL_DOT, Color.Lime, collectPos, 0.05f, 0);
+
+ float rangeOffset = Range + (offset * 2); // because range check starts from collectPos but cone starts from conePos
+ float baseRadius = rangeOffset * (float)Math.Tan(coneAngle);
+
+ //MySimpleObjectDraw.DrawTransparentCone(ref coneMatrix, baseRadius, rangeWithOffset, ref color, 16, Mod.MATERIAL_SQUARE);
+
+ var apexPosition = coneMatrix.Translation;
+ var directionVector = coneMatrix.Forward * rangeOffset;
+ var maxPosCenter = conePos + coneMatrix.Forward * rangeOffset;
+ var baseVector = coneMatrix.Up * baseRadius;
+
+ Vector3 axis = directionVector;
+ axis.Normalize();
+
+ float stepAngle = (float)(Math.PI * 2.0 / (double)WIRE_DIV_RATIO);
+
+ var prevConePoint = apexPosition + directionVector + Vector3.Transform(baseVector, Matrix.CreateFromAxisAngle(axis, (-1 * stepAngle)));
+ prevConePoint = (apexPosition + Vector3D.Normalize((prevConePoint - apexPosition)) * rangeOffset);
+
+ var quad = default(MyQuadD);
+
+ for(int step = 0; step < WIRE_DIV_RATIO; step++)
+ {
+ var conePoint = apexPosition + directionVector + Vector3.Transform(baseVector, Matrix.CreateFromAxisAngle(axis, (step * stepAngle)));
+ var lineDir = (conePoint - apexPosition);
+ lineDir.Normalize();
+ conePoint = (apexPosition + lineDir * rangeOffset);
+
+ MyTransparentGeometry.AddLineBillboard(Mod.MATERIAL_SQUARE, color, conePoint, (prevConePoint - conePoint), 1f, LINE_THICK);
+
+ MyTransparentGeometry.AddLineBillboard(Mod.MATERIAL_SQUARE, color, apexPosition, lineDir, rangeOffset, LINE_THICK);
+
+ MyTransparentGeometry.AddLineBillboard(Mod.MATERIAL_SQUARE, color, conePoint, (maxPosCenter - conePoint), 1f, LINE_THICK);
+
+ // Unusable because SQUARE has reflectivity and this method uses materials' reflectivity... making it unable to be made transparent, also reflective xD
+ //var normal = Vector3.Up;
+ //MyTransparentGeometry.AddTriangleBillboard(
+ // apexPosition, prevConePoint, conePoint,
+ // normal, normal, normal,
+ // new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1),
+ // Mod.MATERIAL_SQUARE, uint.MaxValue, conePoint, planeColor);
+ // also NOTE: if triangle is used, color needs .ToLinearRGB().
+
+ quad.Point0 = prevConePoint;
+ quad.Point1 = conePoint;
+ quad.Point2 = apexPosition;
+ quad.Point3 = apexPosition;
+ MyTransparentGeometry.AddQuad(Mod.MATERIAL_SQUARE, ref quad, planeColor, ref Vector3D.Zero);
+
+ quad.Point0 = prevConePoint;
+ quad.Point1 = conePoint;
+ quad.Point2 = maxPosCenter;
+ quad.Point3 = maxPosCenter;
+ MyTransparentGeometry.AddQuad(Mod.MATERIAL_SQUARE, ref quad, planeColor, ref Vector3D.Zero);
+
+ prevConePoint = conePoint;
+ }
+ }
+
+ bool LoadSettings()
+ {
+ if(block.Storage == null)
+ return false;
+
+ string rawData;
+ if(!block.Storage.TryGetValue(SETTINGS_GUID, out rawData))
+ return false;
+
+ try
+ {
+ var loadedSettings = MyAPIGateway.Utilities.SerializeFromBinary(Convert.FromBase64String(rawData));
+
+ if(loadedSettings != null)
+ {
+ Settings.Range = loadedSettings.Range;
+ Settings.Strength = loadedSettings.Strength;
+ return true;
+ }
+ }
+ catch(Exception e)
+ {
+ Log.Error($"Error loading settings!\n{e}");
+ }
+
+ return false;
+ }
+
+ bool ParseLegacyNameStorage()
+ {
+ string name = block.CustomName.TrimEnd(' ');
+
+ if(!name.EndsWith("]", StringComparison.Ordinal))
+ return false;
+
+ int startIndex = name.IndexOf('[');
+
+ if(startIndex == -1)
+ return false;
+
+ var settingsStr = name.Substring(startIndex + 1, name.Length - startIndex - 2);
+
+ if(settingsStr.Length == 0)
+ return false;
+
+ string[] args = settingsStr.Split(';');
+
+ if(args.Length == 0)
+ return false;
+
+ string[] data;
+
+ foreach(string arg in args)
+ {
+ data = arg.Split('=');
+
+ float f;
+ int i;
+
+ if(data.Length == 2)
+ {
+ switch(data[0])
+ {
+ case "range":
+ if(int.TryParse(data[1], out i))
+ Range = i;
+ break;
+ case "str":
+ if(float.TryParse(data[1], out f))
+ StrengthMul = f;
+ break;
+ }
+ }
+ }
+
+ block.CustomName = name.Substring(0, startIndex).Trim();
+ return true;
+ }
+
+ void SaveSettings()
+ {
+ if(block == null)
+ return; // called too soon or after it was already closed, ignore
+
+ if(Settings == null)
+ throw new NullReferenceException($"Settings == null on entId={Entity?.EntityId}; modInstance={GravityCollectorMod.Instance != null}");
+
+ if(MyAPIGateway.Utilities == null)
+ throw new NullReferenceException($"MyAPIGateway.Utilities == null; entId={Entity?.EntityId}; modInstance={GravityCollectorMod.Instance != null}");
+
+ if(block.Storage == null)
+ block.Storage = new MyModStorageComponent();
+
+ block.Storage.SetValue(SETTINGS_GUID, Convert.ToBase64String(MyAPIGateway.Utilities.SerializeToBinary(Settings)));
+ }
+
+ void SettingsChanged()
+ {
+ if(syncCountdown == 0)
+ syncCountdown = SETTINGS_CHANGED_COUNTDOWN;
+ }
+
+ void SyncSettings()
+ {
+ if(syncCountdown > 0 && --syncCountdown <= 0)
+ {
+ SaveSettings();
+
+ Mod.CachedPacketSettings.Send(block.EntityId, Settings);
+ }
+ }
+
+ public override bool IsSerialized()
+ {
+ // called when the game iterates components to check if they should be serialized, before they're actually serialized.
+ // this does not only include saving but also streaming and blueprinting.
+ // NOTE for this to work reliably the MyModStorageComponent needs to already exist in this block with at least one element.
+
+ try
+ {
+ SaveSettings();
+ }
+ catch(Exception e)
+ {
+ Log.Error(e);
+ }
+
+ return base.IsSerialized();
+ }
+
+ ///
+ /// Returns the specified percentage multiplier (0 to 1) between min and max.
+ ///
+ static float MinMaxPercent(float min, float max, float percentMul)
+ {
+ return min + (percentMul * (max - min));
+ }
+
+ #region Terminal controls
+ static void SetupTerminalControls()
+ {
+ var mod = GravityCollectorMod.Instance;
+
+ if(mod.ControlsCreated)
+ return;
+
+ mod.ControlsCreated = true;
+
+ var controlRange = MyAPIGateway.TerminalControls.CreateControl(CONTROLS_PREFIX + "Range");
+ controlRange.Title = MyStringId.GetOrCompute("Pull Range");
+ controlRange.Tooltip = MyStringId.GetOrCompute("Max distance the cone extends to.");
+ controlRange.Visible = Control_Visible;
+ controlRange.SupportsMultipleBlocks = true;
+ controlRange.SetLimits(Control_Range_Min, Control_Range_Max);
+ controlRange.Getter = Control_Range_Getter;
+ controlRange.Setter = Control_Range_Setter;
+ controlRange.Writer = Control_Range_Writer;
+ MyAPIGateway.TerminalControls.AddControl(controlRange);
+
+ var controlStrength = MyAPIGateway.TerminalControls.CreateControl(CONTROLS_PREFIX + "Strength");
+ controlStrength.Title = MyStringId.GetOrCompute("Pull Strength");
+ controlStrength.Tooltip = MyStringId.GetOrCompute($"Formula used:\nForce = Min(ObjectMass * {MASS_MUL.ToString()}, {MAX_MASS.ToString()}) * (Strength / 100)");
+ controlStrength.Visible = Control_Visible;
+ controlStrength.SupportsMultipleBlocks = true;
+ controlStrength.SetLimits(STRENGTH_MIN, STRENGTH_MAX);
+ controlStrength.Getter = Control_Strength_Getter;
+ controlStrength.Setter = Control_Strength_Setter;
+ controlStrength.Writer = Control_Strength_Writer;
+ MyAPIGateway.TerminalControls.AddControl(controlStrength);
+ }
+
+ static GravityCollector GetLogic(IMyTerminalBlock block) => block?.GameLogic?.GetAs();
+
+ static bool Control_Visible(IMyTerminalBlock block)
+ {
+ return GetLogic(block) != null;
+ }
+
+ static float Control_Strength_Getter(IMyTerminalBlock block)
+ {
+ var logic = GetLogic(block);
+ return (logic == null ? STRENGTH_MIN : logic.StrengthMul * 100);
+ }
+
+ static void Control_Strength_Setter(IMyTerminalBlock block, float value)
+ {
+ var logic = GetLogic(block);
+ if(logic != null)
+ logic.StrengthMul = ((int)value / 100f);
+ }
+
+ static void Control_Strength_Writer(IMyTerminalBlock block, StringBuilder writer)
+ {
+ var logic = GetLogic(block);
+ if(logic != null)
+ writer.Append((int)(logic.StrengthMul * 100f)).Append('%');
+ }
+
+ static float Control_Range_Getter(IMyTerminalBlock block)
+ {
+ var logic = GetLogic(block);
+ return (logic == null ? 0 : logic.Range);
+ }
+
+ static void Control_Range_Setter(IMyTerminalBlock block, float value)
+ {
+ var logic = GetLogic(block);
+ if(logic != null)
+ logic.Range = (int)Math.Floor(value);
+ }
+
+ static float Control_Range_Min(IMyTerminalBlock block)
+ {
+ return RANGE_MIN;
+ }
+
+ static float Control_Range_Max(IMyTerminalBlock block)
+ {
+ var logic = GetLogic(block);
+ return (logic == null ? 0 : logic.maxRange);
+ }
+
+ static void Control_Range_Writer(IMyTerminalBlock block, StringBuilder writer)
+ {
+ var logic = GetLogic(block);
+ if(logic != null)
+ {
+ if(logic.Range < RANGE_OFF_EXCLUSIVE)
+ writer.Append("OFF");
+ else
+ writer.Append(logic.Range.ToString("N2")).Append(" m");
+ }
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollectorBlockSettings.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollectorBlockSettings.cs
new file mode 100644
index 00000000..ca44d34c
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollectorBlockSettings.cs
@@ -0,0 +1,14 @@
+using ProtoBuf;
+
+namespace Digi.GravityCollector
+{
+ [ProtoContract(UseProtoMembersOnly = true)]
+ public class GravityCollectorBlockSettings
+ {
+ [ProtoMember(1)]
+ public float Range;
+
+ [ProtoMember(2)]
+ public float Strength;
+ }
+}
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollectorMod.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollectorMod.cs
new file mode 100644
index 00000000..466216d7
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/GravityCollectorMod.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using Digi.GravityCollector.Sync;
+using VRage.Game.Components;
+using VRage.Game.Entity;
+using VRage.Utils;
+
+namespace Digi.GravityCollector
+{
+ [MySessionComponentDescriptor(MyUpdateOrder.NoUpdate)]
+ public class GravityCollectorMod : MySessionComponentBase
+ {
+ public static GravityCollectorMod Instance;
+
+ public bool ControlsCreated = false;
+ public Networking Networking = new Networking(58936);
+ public List Entities = new List();
+ public PacketBlockSettings CachedPacketSettings;
+
+ public readonly MyStringId MATERIAL_SQUARE = MyStringId.GetOrCompute("Square");
+ public readonly MyStringId MATERIAL_DOT = MyStringId.GetOrCompute("WhiteDot");
+
+ public override void LoadData()
+ {
+ Instance = this;
+
+ Networking.Register();
+
+ CachedPacketSettings = new PacketBlockSettings();
+ }
+
+ protected override void UnloadData()
+ {
+ Instance = null;
+
+ Networking?.Unregister();
+ Networking = null;
+ }
+ }
+}
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/Log.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Log.cs
new file mode 100644
index 00000000..0c5c6c41
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Log.cs
@@ -0,0 +1,481 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using ParallelTasks;
+using Sandbox.ModAPI;
+using VRage.Game;
+using VRage.Game.Components;
+using VRage.Game.ModAPI;
+using VRage.Utils;
+
+namespace Digi
+{
+ ///
+ /// Standalone logger, does not require any setup.
+ /// Mod name is automatically set from workshop name or folder name. Can also be manually defined using .
+ /// Version 1.52 by Digi
+ ///
+ [MySessionComponentDescriptor(MyUpdateOrder.NoUpdate, priority: int.MaxValue)]
+ public class Log : MySessionComponentBase
+ {
+ private static Log instance;
+ private static Handler handler;
+ private static bool unloaded = false;
+
+ public const string FILE = "info.log";
+ private const int DEFAULT_TIME_INFO = 3000;
+ private const int DEFAULT_TIME_ERROR = 10000;
+
+ ///
+ /// Print the generic error info.
+ /// (For use in 's 2nd arg)
+ ///
+ public const string PRINT_ERROR = "";
+
+ ///
+ /// Prints the message instead of the generic generated error info.
+ /// (For use in 's 2nd arg)
+ ///
+ public const string PRINT_MSG = "";
+
+ #region Handling of handler
+ public override void LoadData()
+ {
+ instance = this;
+ EnsureHandlerCreated();
+ handler.Init(this);
+ }
+
+ protected override void UnloadData()
+ {
+ instance = null;
+
+ if(handler != null && handler.AutoClose)
+ {
+ Unload();
+ }
+ }
+
+ private void Unload()
+ {
+ try
+ {
+ if(unloaded)
+ return;
+
+ unloaded = true;
+ handler?.Close();
+ handler = null;
+ }
+ catch(Exception e)
+ {
+ MyLog.Default.WriteLine($"Error in {ModContext.ModName} ({ModContext.ModId}): {e.Message}\n{e.StackTrace}");
+ throw new ModCrashedException(e, ModContext);
+ }
+ }
+
+ private static void EnsureHandlerCreated()
+ {
+ if(unloaded)
+ throw new Exception("Digi.Log accessed after it was unloaded!");
+
+ if(handler == null)
+ handler = new Handler();
+ }
+ #endregion
+
+ #region Publicly accessible properties and methods
+ ///
+ /// Manually unload the logger. Works regardless of , but if that property is false then this method must be called!
+ ///
+ public static void Close()
+ {
+ instance?.Unload();
+ }
+
+ ///
+ /// Defines if the component self-unloads next tick or after .
+ /// If set to false, you must call manually!
+ ///
+ public static bool AutoClose
+ {
+ get
+ {
+ EnsureHandlerCreated();
+ return handler.AutoClose;
+ }
+ set
+ {
+ EnsureHandlerCreated();
+ handler.AutoClose = value;
+ }
+ }
+
+ ///
+ /// Sets/gets the mod name.
+ /// This is optional as the mod name is generated from the folder/workshop name, but those can be weird or long.
+ ///
+ public static string ModName
+ {
+ get
+ {
+ EnsureHandlerCreated();
+ return handler.ModName;
+ }
+ set
+ {
+ EnsureHandlerCreated();
+ handler.ModName = value;
+ }
+ }
+
+ ///
+ /// Gets the workshop id of the mod.
+ /// Will return 0 if it's a local mod or if it's called before LoadData() executes on the logger.
+ ///
+ public static ulong WorkshopId => handler?.WorkshopId ?? 0;
+
+ ///
+ /// Increases indentation by 4 spaces.
+ /// Each indent adds 4 space characters before each of the future messages.
+ ///
+ public static void IncreaseIndent()
+ {
+ EnsureHandlerCreated();
+ handler.IncreaseIndent();
+ }
+
+ ///
+ /// Decreases indentation by 4 space characters down to 0 indentation.
+ /// See
+ ///
+ public static void DecreaseIndent()
+ {
+ EnsureHandlerCreated();
+ handler.DecreaseIndent();
+ }
+
+ ///
+ /// Resets the indentation to 0.
+ /// See
+ ///
+ public static void ResetIndent()
+ {
+ EnsureHandlerCreated();
+ handler.ResetIndent();
+ }
+
+ ///
+ /// Writes an exception to custom log file, game's log file and by default writes a generic error message to player's HUD.
+ ///
+ /// The exception to write to custom log and game's log.
+ /// HUD notification text, can be set to null to disable, to to use the exception message, to use the predefined error message, or any other custom string.
+ /// How long to show the HUD notification for, in miliseconds.
+ public static void Error(Exception exception, string printText = PRINT_ERROR, int printTimeMs = DEFAULT_TIME_ERROR)
+ {
+ EnsureHandlerCreated();
+ handler.Error(exception.ToString(), printText, printTimeMs);
+ }
+
+ ///
+ /// Writes a message to custom log file, game's log file and by default writes a generic error message to player's HUD.
+ ///
+ /// The message printed to custom log and game log.
+ /// HUD notification text, can be set to null to disable, to to use the message arg, to use the predefined error message, or any other custom string.
+ /// How long to show the HUD notification for, in miliseconds.
+ public static void Error(string message, string printText = PRINT_ERROR, int printTimeMs = DEFAULT_TIME_ERROR)
+ {
+ EnsureHandlerCreated();
+ handler.Error(message, printText, printTimeMs);
+ }
+
+ ///
+ /// Writes a message in the custom log file.
+ /// Optionally prints a different message (or same message) in player's HUD.
+ ///
+ /// The text that's written to log.
+ /// HUD notification text, can be set to null to disable, to to use the message arg or any other custom string.
+ /// How long to show the HUD notification for, in miliseconds.
+ public static void Info(string message, string printText = null, int printTimeMs = DEFAULT_TIME_INFO)
+ {
+ EnsureHandlerCreated();
+ handler.Info(message, printText, printTimeMs);
+ }
+
+ ///
+ /// Iterates task errors and reports them, returns true if any errors were found.
+ ///
+ /// The task to check for errors.
+ /// Used in the reports.
+ /// true if errors found, false otherwise.
+ public static bool TaskHasErrors(Task task, string taskName)
+ {
+ EnsureHandlerCreated();
+
+ if(task.Exceptions != null && task.Exceptions.Length > 0)
+ {
+ foreach(var e in task.Exceptions)
+ {
+ Error($"Error in {taskName} thread!\n{e}");
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ #endregion
+
+ private class Handler
+ {
+ private Log sessionComp;
+ private string modName = string.Empty;
+
+ private TextWriter writer;
+ private int indent = 0;
+ private string errorPrintText;
+
+ private IMyHudNotification notifyInfo;
+ private IMyHudNotification notifyError;
+
+ private StringBuilder sb = new StringBuilder(64);
+
+ private List preInitMessages;
+
+ public bool AutoClose { get; set; } = true;
+
+ public ulong WorkshopId { get; private set; } = 0;
+
+ public string ModName
+ {
+ get
+ {
+ return modName;
+ }
+ set
+ {
+ modName = value;
+ ComputeErrorPrintText();
+ }
+ }
+
+ public Handler()
+ {
+ }
+
+ public void Init(Log sessionComp)
+ {
+ if(writer != null)
+ return; // already initialized
+
+ if(MyAPIGateway.Utilities == null)
+ {
+ Error("MyAPIGateway.Utilities is NULL !");
+ return;
+ }
+
+ this.sessionComp = sessionComp;
+
+ if(string.IsNullOrWhiteSpace(ModName))
+ ModName = sessionComp.ModContext.ModName;
+
+ WorkshopId = GetWorkshopID(sessionComp.ModContext.ModId);
+
+ writer = MyAPIGateway.Utilities.WriteFileInLocalStorage(FILE, typeof(Log));
+
+ #region Pre-init messages
+ if(preInitMessages != null)
+ {
+ string warning = $"{modName} WARNING: there are log messages before the mod initialized!";
+
+ Info($"--- pre-init messages ---");
+
+ foreach(var msg in preInitMessages)
+ {
+ Info(msg, warning);
+ }
+
+ Info("--- end pre-init messages ---");
+
+ preInitMessages = null;
+ }
+ #endregion
+
+ #region Init message
+ sb.Clear();
+ sb.Append("Initialized");
+ sb.Append("\nGameMode=").Append(MyAPIGateway.Session.SessionSettings.GameMode);
+ sb.Append("\nOnlineMode=").Append(MyAPIGateway.Session.SessionSettings.OnlineMode);
+ sb.Append("\nServer=").Append(MyAPIGateway.Session.IsServer);
+ sb.Append("\nDS=").Append(MyAPIGateway.Utilities.IsDedicated);
+ sb.Append("\nDefined=");
+
+#if STABLE
+ sb.Append("STABLE, ");
+#endif
+
+#if UNOFFICIAL
+ sb.Append("UNOFFICIAL, ");
+#endif
+
+#if DEBUG
+ sb.Append("DEBUG, ");
+#endif
+
+#if BRANCH_STABLE
+ sb.Append("BRANCH_STABLE, ");
+#endif
+
+#if BRANCH_DEVELOP
+ sb.Append("BRANCH_DEVELOP, ");
+#endif
+
+#if BRANCH_UNKNOWN
+ sb.Append("BRANCH_UNKNOWN, ");
+#endif
+
+ Info(sb.ToString());
+ sb.Clear();
+ #endregion
+ }
+
+ public void Close()
+ {
+ if(writer != null)
+ {
+ Info("Unloaded.");
+
+ writer.Flush();
+ writer.Close();
+ writer = null;
+ }
+ }
+
+ private void ComputeErrorPrintText()
+ {
+ errorPrintText = $"[ {modName} ERROR, report contents of: %AppData%/SpaceEngineers/Storage/{MyAPIGateway.Utilities.GamePaths.ModScopeName}/{FILE} ]";
+ }
+
+ public void IncreaseIndent()
+ {
+ indent++;
+ }
+
+ public void DecreaseIndent()
+ {
+ if(indent > 0)
+ indent--;
+ }
+
+ public void ResetIndent()
+ {
+ indent = 0;
+ }
+
+ public void Error(string message, string printText = PRINT_ERROR, int printTime = DEFAULT_TIME_ERROR)
+ {
+ MyLog.Default.WriteLineAndConsole(modName + " error/exception: " + message); // write to game's log
+
+ LogMessage(message, "ERROR: "); // write to custom log
+
+ if(printText != null) // printing to HUD is optional
+ ShowHudMessage(ref notifyError, message, printText, printTime, MyFontEnum.Red);
+ }
+
+ public void Info(string message, string printText = null, int printTime = DEFAULT_TIME_INFO)
+ {
+ LogMessage(message); // write to custom log
+
+ if(printText != null) // printing to HUD is optional
+ ShowHudMessage(ref notifyInfo, message, printText, printTime, MyFontEnum.White);
+ }
+
+ private void ShowHudMessage(ref IMyHudNotification notify, string message, string printText, int printTime, string font)
+ {
+ if(printText == null)
+ return;
+
+ try
+ {
+ if(MyAPIGateway.Utilities != null && !MyAPIGateway.Utilities.IsDedicated)
+ {
+ if(printText == PRINT_ERROR)
+ printText = errorPrintText;
+ else if(printText == PRINT_MSG)
+ printText = $"[ {modName} ERROR: {message} ]";
+
+ if(notify == null)
+ {
+ notify = MyAPIGateway.Utilities.CreateNotification(printText, printTime, font);
+ }
+ else
+ {
+ notify.Text = printText;
+ notify.AliveTime = printTime;
+ notify.ResetAliveTime();
+ }
+
+ notify.Show();
+ }
+ }
+ catch(Exception e)
+ {
+ Info("ERROR: Could not send notification to local client: " + e);
+ MyLog.Default.WriteLineAndConsole(modName + " logger error/exception: Could not send notification to local client: " + e);
+ }
+ }
+
+ private void LogMessage(string message, string prefix = null)
+ {
+ try
+ {
+ sb.Clear();
+ sb.Append(DateTime.Now.ToString("[HH:mm:ss] "));
+
+ if(writer == null)
+ sb.Append("(PRE-INIT) ");
+
+ for(int i = 0; i < indent; i++)
+ sb.Append(' ', 4);
+
+ if(prefix != null)
+ sb.Append(prefix);
+
+ sb.Append(message);
+
+ if(writer == null)
+ {
+ if(preInitMessages == null)
+ preInitMessages = new List();
+
+ preInitMessages.Add(sb.ToString());
+ }
+ else
+ {
+ writer.WriteLine(sb);
+ writer.Flush();
+ }
+
+ sb.Clear();
+ }
+ catch(Exception e)
+ {
+ MyLog.Default.WriteLineAndConsole($"{modName} had an error while logging message = '{message}'\nLogger error: {e.Message}\n{e.StackTrace}");
+ }
+ }
+
+ private ulong GetWorkshopID(string modId)
+ {
+ // NOTE workaround for MyModContext not having the actual workshop ID number.
+ foreach(var mod in MyAPIGateway.Session.Mods)
+ {
+ if(mod.Name == modId)
+ return mod.PublishedFileId;
+ }
+
+ return 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/Networking.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/Networking.cs
new file mode 100644
index 00000000..68a3fc03
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/Networking.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using Sandbox.ModAPI;
+using VRage.Game.ModAPI;
+
+namespace Digi.GravityCollector.Sync
+{
+ public class Networking
+ {
+ public readonly ushort PacketId;
+
+ ///
+ /// must be unique from all other mods that also use packets.
+ ///
+ public Networking(ushort packetId)
+ {
+ PacketId = packetId;
+ }
+
+ ///
+ /// Register packet monitoring, not necessary if you don't want the local machine to handle incomming packets.
+ ///
+ public void Register()
+ {
+ MyAPIGateway.Multiplayer.RegisterMessageHandler(PacketId, ReceivedPacket);
+ }
+
+ ///
+ /// This must be called on world unload if you called .
+ ///
+ public void Unregister()
+ {
+ MyAPIGateway.Multiplayer.UnregisterMessageHandler(PacketId, ReceivedPacket);
+ }
+
+ private void ReceivedPacket(byte[] rawData) // executed when a packet is received on this machine
+ {
+ try
+ {
+ var packet = MyAPIGateway.Utilities.SerializeFromBinary(rawData);
+
+ bool relay = false;
+ packet.Received(ref relay);
+
+ if(relay)
+ RelayToClients(packet, rawData);
+ }
+ catch(Exception e)
+ {
+ Log.Error(e);
+ }
+ }
+
+ ///
+ /// Send a packet to the server.
+ /// Works from clients and server.
+ ///
+ ///
+ public void SendToServer(PacketBase packet)
+ {
+ var bytes = MyAPIGateway.Utilities.SerializeToBinary(packet);
+
+ MyAPIGateway.Multiplayer.SendMessageToServer(PacketId, bytes);
+ }
+
+ ///
+ /// Send a packet to a specific player.
+ /// Only works server side.
+ ///
+ ///
+ ///
+ public void SendToPlayer(PacketBase packet, ulong steamId)
+ {
+ if(!MyAPIGateway.Multiplayer.IsServer)
+ return;
+
+ var bytes = MyAPIGateway.Utilities.SerializeToBinary(packet);
+
+ MyAPIGateway.Multiplayer.SendMessageTo(PacketId, bytes, steamId);
+ }
+
+ private List tempPlayers;
+
+ ///
+ /// Sends packet (or supplied bytes) to all players except server player and supplied packet's sender.
+ /// Only works server side.
+ ///
+ public void RelayToClients(PacketBase packet, byte[] rawData = null)
+ {
+ if(!MyAPIGateway.Multiplayer.IsServer)
+ return;
+
+ if(tempPlayers == null)
+ tempPlayers = new List(MyAPIGateway.Session.SessionSettings.MaxPlayers);
+ else
+ tempPlayers.Clear();
+
+ MyAPIGateway.Players.GetPlayers(tempPlayers);
+
+ foreach(var p in tempPlayers)
+ {
+ if(p.SteamUserId == MyAPIGateway.Multiplayer.ServerId)
+ continue;
+
+ if(p.SteamUserId == packet.SenderId)
+ continue;
+
+ if(rawData == null)
+ rawData = MyAPIGateway.Utilities.SerializeToBinary(packet);
+
+ MyAPIGateway.Multiplayer.SendMessageTo(PacketId, rawData, p.SteamUserId);
+ }
+
+ tempPlayers.Clear();
+ }
+ }
+}
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/PacketBase.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/PacketBase.cs
new file mode 100644
index 00000000..3b86f4dd
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/PacketBase.cs
@@ -0,0 +1,26 @@
+using ProtoBuf;
+using Sandbox.ModAPI;
+
+namespace Digi.GravityCollector.Sync
+{
+ [ProtoInclude(2, typeof(PacketBlockSettings))]
+ [ProtoContract(UseProtoMembersOnly = true)]
+ public abstract class PacketBase
+ {
+ [ProtoMember(1)]
+ public readonly ulong SenderId;
+
+ protected Networking Networking => GravityCollectorMod.Instance.Networking;
+
+ public PacketBase()
+ {
+ SenderId = MyAPIGateway.Multiplayer.MyId;
+ }
+
+ ///
+ /// Called when this packet is received on this machine.
+ ///
+ /// Set to true to relay this packet to clients, only works server side.
+ public abstract void Received(ref bool relay);
+ }
+}
diff --git a/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/PacketBlockSettings.cs b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/PacketBlockSettings.cs
new file mode 100644
index 00000000..e4b4d557
--- /dev/null
+++ b/Advanced Gravity Collector/Data/Scripts/GravityCollector/Sync/PacketBlockSettings.cs
@@ -0,0 +1,46 @@
+using ProtoBuf;
+using Sandbox.ModAPI;
+
+namespace Digi.GravityCollector.Sync
+{
+ [ProtoContract(UseProtoMembersOnly = true)]
+ public class PacketBlockSettings : PacketBase
+ {
+ [ProtoMember(1)]
+ public long EntityId;
+
+ [ProtoMember(2)]
+ public GravityCollectorBlockSettings Settings;
+
+ public PacketBlockSettings() { } // Empty constructor required for deserialization
+
+ public void Send(long entityId, GravityCollectorBlockSettings settings)
+ {
+ EntityId = entityId;
+ Settings = settings;
+
+ if(MyAPIGateway.Multiplayer.IsServer)
+ Networking.RelayToClients(this);
+ else
+ Networking.SendToServer(this);
+ }
+
+ public override void Received(ref bool relay)
+ {
+ var block = MyAPIGateway.Entities.GetEntityById(this.EntityId) as IMyCollector;
+
+ if(block == null)
+ return;
+
+ var logic = block.GameLogic?.GetAs();
+
+ if(logic == null)
+ return;
+
+ logic.Settings.Range = this.Settings.Range;
+ logic.Settings.Strength = this.Settings.Strength;
+
+ relay = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Advanced Gravity Collector/Models/LargeGravityCollector.mwm b/Advanced Gravity Collector/Models/LargeGravityCollector.mwm
new file mode 100644
index 00000000..f04d196c
Binary files /dev/null and b/Advanced Gravity Collector/Models/LargeGravityCollector.mwm differ
diff --git a/Advanced Gravity Collector/Models/LargeGravityCollector_LOD1.mwm b/Advanced Gravity Collector/Models/LargeGravityCollector_LOD1.mwm
new file mode 100644
index 00000000..e0a3c351
Binary files /dev/null and b/Advanced Gravity Collector/Models/LargeGravityCollector_LOD1.mwm differ
diff --git a/Advanced Gravity Collector/Models/LargeGravityCollector_LOD2.mwm b/Advanced Gravity Collector/Models/LargeGravityCollector_LOD2.mwm
new file mode 100644
index 00000000..bc643237
Binary files /dev/null and b/Advanced Gravity Collector/Models/LargeGravityCollector_LOD2.mwm differ
diff --git a/Advanced Gravity Collector/Models/LargeGravityCollector_LOD3.mwm b/Advanced Gravity Collector/Models/LargeGravityCollector_LOD3.mwm
new file mode 100644
index 00000000..f1b345b7
Binary files /dev/null and b/Advanced Gravity Collector/Models/LargeGravityCollector_LOD3.mwm differ
diff --git a/Advanced Gravity Collector/Models/MediumGravityCollector.mwm b/Advanced Gravity Collector/Models/MediumGravityCollector.mwm
new file mode 100644
index 00000000..d6bc8bd8
Binary files /dev/null and b/Advanced Gravity Collector/Models/MediumGravityCollector.mwm differ
diff --git a/Advanced Gravity Collector/Models/MediumGravityCollector_LOD1.mwm b/Advanced Gravity Collector/Models/MediumGravityCollector_LOD1.mwm
new file mode 100644
index 00000000..58726a10
Binary files /dev/null and b/Advanced Gravity Collector/Models/MediumGravityCollector_LOD1.mwm differ
diff --git a/Advanced Gravity Collector/Models/MediumGravityCollector_LOD2.mwm b/Advanced Gravity Collector/Models/MediumGravityCollector_LOD2.mwm
new file mode 100644
index 00000000..d303f926
Binary files /dev/null and b/Advanced Gravity Collector/Models/MediumGravityCollector_LOD2.mwm differ
diff --git a/Advanced Gravity Collector/Models/MediumGravityCollector_LOD3.mwm b/Advanced Gravity Collector/Models/MediumGravityCollector_LOD3.mwm
new file mode 100644
index 00000000..c2100020
Binary files /dev/null and b/Advanced Gravity Collector/Models/MediumGravityCollector_LOD3.mwm differ
diff --git a/Advanced Gravity Collector/Textures/Icons/GravityCollector.dds b/Advanced Gravity Collector/Textures/Icons/GravityCollector.dds
new file mode 100644
index 00000000..fc7bd152
Binary files /dev/null and b/Advanced Gravity Collector/Textures/Icons/GravityCollector.dds differ