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