diff --git a/GodotHelper.Tests/GodotHelper.Tests.csproj b/GodotHelper.Tests/GodotHelper.Tests.csproj index e109ad3..65195e2 100644 --- a/GodotHelper.Tests/GodotHelper.Tests.csproj +++ b/GodotHelper.Tests/GodotHelper.Tests.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 disable enable true diff --git a/GodotHelper.Tests/test/src/MyNodeExtensionsTest.cs b/GodotHelper.Tests/test/src/MyNodeExtensionsTest.cs new file mode 100644 index 0000000..4fa8db4 --- /dev/null +++ b/GodotHelper.Tests/test/src/MyNodeExtensionsTest.cs @@ -0,0 +1,34 @@ +namespace GodotHelper.Tests; + +using System; +using System.Threading.Tasks; +using Chickensoft.GoDotTest; +using Godot; +using GodotHelpers; +using Shouldly; + +public class MyNodeExtensionsTest : TestClass +{ + public MyNodeExtensionsTest(Node testScene) : base(testScene) { } + + [Test] + public async Task FreeDeferred() + { + var test = new Node(); + var freeDeferred = new Node(); + TestScene.AddChild(test); + test.AddChild(freeDeferred); + var tree = TestScene.GetTree(); + await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame); + + // Console.Write(test.GetChildCount()); + freeDeferred.FreeDeferred(); + + test.GetChildCount().ShouldBe(1); + + await tree.ToSignal(tree, SceneTree.SignalName.ProcessFrame); + test.GetChildCount().ShouldBe(0); + + } + +} diff --git a/GodotHelper/GodotHelper.csproj b/GodotHelper/GodotHelper.csproj index fbb611c..5bcb511 100644 --- a/GodotHelper/GodotHelper.csproj +++ b/GodotHelper/GodotHelper.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 true preview true @@ -48,5 +48,6 @@ all + diff --git a/GodotHelper/src/Math/LerpVectors.cs b/GodotHelper/src/Math/LerpVectors.cs new file mode 100644 index 0000000..a05c006 --- /dev/null +++ b/GodotHelper/src/Math/LerpVectors.cs @@ -0,0 +1,24 @@ +namespace GodotHelpers; + +using Godot; +public static partial class SJKMath +{ + // public static Vector3I Lerp(Vector3I from, Vector3I to, float weight) => new Vector3I( + // Mathf.Lerp(from.X, to.X, weight), + // Mathf.Lerp(from.Y, to.Y, weight), + // Mathf.Lerp(from.Z, to.Z, weight) + // ); + public static Vector3 Lerp(Vector3 from, Vector3 to, float weight) => new Vector3( + Mathf.Lerp(from.X, to.X, weight), + Mathf.Lerp(from.Y, to.Y, weight), + Mathf.Lerp(from.Z, to.Z, weight) + ); + // public static Vector2I Lerp(Vector2I from, Vector2I to, float weight) => new Vector2I( + // Mathf.Lerp(from.X, to.X, weight), + // Mathf.Lerp(from.Y, to.Y, weight) + // ); + public static Vector2 Lerp(Vector2 from, Vector2 to, float weight) => new Vector2( + Mathf.Lerp(from.X, to.X, weight), + Mathf.Lerp(from.Y, to.Y, weight) + ); +} diff --git a/GodotHelper/src/MyNodeExtensions.cs b/GodotHelper/src/MyNodeExtensions.cs new file mode 100644 index 0000000..579c160 --- /dev/null +++ b/GodotHelper/src/MyNodeExtensions.cs @@ -0,0 +1,46 @@ +namespace GodotHelpers; + +using System; +using System.Linq; +using Godot; + + +public static class MyNodeExtensions +{ + public static void FreeDeferred(this Node node) => node.CallDeferred(GodotObject.MethodName.Free); + /// + /// Checks if given Property exists on Base + /// + /// Current GodotObject + /// Property Name + /// bool true if given property exists on Base + public static bool HasProperty(this GodotObject Base, string PropertyName) + { + /* + Returns the object's property list as an Godot.Collections.Array of dictionaries. + Each Godot.Collections.Dictionary contains the following entries: + - name is the property's name, as a string; + - class_name is an empty StringName, unless the property is Variant.Type.Object and it inherits from a class; + - type is the property's type, as an int (see Variant.Type); + - hint is how the property is meant to be edited (see PropertyHint); + - hint_string depends on the hint (see PropertyHint); + - usage is a combination of PropertyUsageFlags. + */ + foreach (var Property in Base.GetPropertyListEx()) + { + if (Property.Name == PropertyName) + { + return true; + } + } + + return false; + } + public static (Node node, Resource resource, NodePath remaining) GetNodeAndResourceEx(this Node self, NodePath path) + { + var result = self.GetNodeAndResource(path); + return ((Node)result[0], (Resource)result[1], (NodePath)result[2]); + } + public static void QueueFreeChildren(this Node node) => node.GetChildren().ToList().ForEach(item => item.QueueFree()); + public static void QueueFreeChildren(this Node node, Func predicate) => node.GetChildren().Where(predicate).ToList().ForEach(item => item.QueueFree()); +} diff --git a/GodotHelper/src/Raycasts/CollisionResultBase.cs b/GodotHelper/src/Raycasts/CollisionResultBase.cs new file mode 100644 index 0000000..e0d6c70 --- /dev/null +++ b/GodotHelper/src/Raycasts/CollisionResultBase.cs @@ -0,0 +1,10 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record CollisionResultBase( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape +); diff --git a/GodotHelper/src/Raycasts/PointQueryResult2D.cs b/GodotHelper/src/Raycasts/PointQueryResult2D.cs new file mode 100644 index 0000000..f93f4e1 --- /dev/null +++ b/GodotHelper/src/Raycasts/PointQueryResult2D.cs @@ -0,0 +1,10 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record PointQueryResult2D( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape +) : CollisionResultBase(Collider, ColliderId, Rid, Shape); diff --git a/GodotHelper/src/Raycasts/PointQueryResult3D.cs b/GodotHelper/src/Raycasts/PointQueryResult3D.cs new file mode 100644 index 0000000..9cbbf3f --- /dev/null +++ b/GodotHelper/src/Raycasts/PointQueryResult3D.cs @@ -0,0 +1,10 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record PointQueryResult3D( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape +) : CollisionResultBase(Collider, ColliderId, Rid, Shape); diff --git a/GodotHelper/src/Raycasts/RaycastExtensions.cs b/GodotHelper/src/Raycasts/RaycastExtensions.cs new file mode 100644 index 0000000..7ce47b1 --- /dev/null +++ b/GodotHelper/src/Raycasts/RaycastExtensions.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Godot; +using Godot.Collections; +using SJK.Functional; + +namespace GodotHelpers.Raycasts; + + +public static class RaycastExtensions +{ + // --- 3D --- + + public static RaycastResult3D? RaycastEx(this PhysicsDirectSpaceState3D space, Vector3 from, Vector3 to, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false) + { + var query = new PhysicsRayQueryParameters3D + { + From = from, + To = to, + CollisionMask = collisionMask, + HitFromInside = hitFromInside, + }; + if (exclude != null) + query.Exclude = new Array(exclude); + + var result = space.IntersectRay(query); + if (result.Count == 0) + return null; + + return new RaycastResult3D( + Collider: result["collider"].AsGodotObject(), + ColliderId: result["collider_id"].AsInt32(), + Rid: (Rid)result["rid"], + Shape: result["shape"].AsInt32(), + Position: result["position"].AsVector3(), + Normal: result["normal"].AsVector3() + ); + } + + public static bool RaycastHitEx(this PhysicsDirectSpaceState3D space, Vector3 from, Vector3 to, [NotNullWhen(true)] out RaycastResult3D hit, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false) + { + hit = space.RaycastEx(from, to, collisionMask, exclude, hitFromInside)!; + return hit is not null; + } + + // --- 2D --- + + public static RaycastResult2D? RaycastEx(this PhysicsDirectSpaceState2D space, Vector2 from, Vector2 to, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false) + { + var query = new PhysicsRayQueryParameters2D + { + From = from, + To = to, + CollisionMask = collisionMask, + HitFromInside = hitFromInside, + }; + if (exclude != null) + query.Exclude = new Array(exclude); + + var result = space.IntersectRay(query); + if (result.Count == 0) + return null; + + return new RaycastResult2D( + Collider: result["collider"].AsGodotObject(), + ColliderId: result["collider_id"].AsInt32(), + Rid: (Rid)result["rid"], + Shape: result["shape"].AsInt32(), + Position: result["position"].AsVector2(), + Normal: result["normal"].AsVector2() + ); + } + public static IOption RaycastOptionEx(this PhysicsDirectSpaceState2D space, Vector2 from, Vector2 to, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false) + => RaycastEx(space, from, to, collisionMask, exclude, hitFromInside).ToOption(); + + + public static bool RaycastHitEx(this PhysicsDirectSpaceState2D space, Vector2 from, Vector2 to, out RaycastResult2D hit, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false) + { + hit = space.RaycastEx(from, to, collisionMask, exclude, hitFromInside)!; + return hit is not null; + } + // 3D Shape Cast + public static List IntersectShapeEx(this PhysicsDirectSpaceState3D space, PhysicsShapeQueryParameters3D query, int maxResults = 32) + { + var results = space.IntersectShape(query, maxResults); + var list = new List(results.Count); + + foreach (var dict in results) + { + list.Add(new CollisionResultBase( + Collider: dict["collider"].AsGodotObject(), + ColliderId: dict["collider_id"].AsInt32(), + Rid: (Rid)dict["rid"], + Shape: dict["shape"].AsInt32() + )); + } + + return list; + } + + // 2D Shape Cast + public static List IntersectShapeEx(this PhysicsDirectSpaceState2D space, PhysicsShapeQueryParameters2D query, int maxResults = 32) + { + var results = space.IntersectShape(query, maxResults); + var list = new List(results.Count); + + foreach (var dict in results) + { + list.Add(new CollisionResultBase( + Collider: dict["collider"].AsGodotObject(), + ColliderId: dict["collider_id"].AsInt32(), + Rid: (Rid)dict["rid"], + Shape: dict["shape"].AsInt32() + )); + } + + return list; + } +} diff --git a/GodotHelper/src/Raycasts/RaycastResult2D.cs b/GodotHelper/src/Raycasts/RaycastResult2D.cs new file mode 100644 index 0000000..18e7302 --- /dev/null +++ b/GodotHelper/src/Raycasts/RaycastResult2D.cs @@ -0,0 +1,12 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record RaycastResult2D( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape, + Vector2 Position, + Vector2 Normal +) : CollisionResultBase(Collider, ColliderId, Rid, Shape); diff --git a/GodotHelper/src/Raycasts/RaycastResult3D.cs b/GodotHelper/src/Raycasts/RaycastResult3D.cs new file mode 100644 index 0000000..3d2e493 --- /dev/null +++ b/GodotHelper/src/Raycasts/RaycastResult3D.cs @@ -0,0 +1,12 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record RaycastResult3D( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape, + Vector3 Position, + Vector3 Normal +) : CollisionResultBase(Collider, ColliderId, Rid, Shape); diff --git a/GodotHelper/src/Raycasts/ShapeCastResult2D.cs b/GodotHelper/src/Raycasts/ShapeCastResult2D.cs new file mode 100644 index 0000000..f9b54a5 --- /dev/null +++ b/GodotHelper/src/Raycasts/ShapeCastResult2D.cs @@ -0,0 +1,13 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record ShapeCastResult2D( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape, + Vector2 Point, + Vector2 Normal, + int CollisionCount +) : CollisionResultBase(Collider, ColliderId, Rid, Shape); diff --git a/GodotHelper/src/Raycasts/ShapeCastResult3D.cs b/GodotHelper/src/Raycasts/ShapeCastResult3D.cs new file mode 100644 index 0000000..ec98499 --- /dev/null +++ b/GodotHelper/src/Raycasts/ShapeCastResult3D.cs @@ -0,0 +1,13 @@ +using Godot; + +namespace GodotHelpers.Raycasts; + +public record ShapeCastResult3D( + GodotObject Collider, + int ColliderId, + Rid Rid, + int Shape, + Vector3 Point, + Vector3 Normal, + int CollisionCount +) : CollisionResultBase(Collider, ColliderId, Rid, Shape); diff --git a/GodotHelper/src/Reflector.cs b/GodotHelper/src/Reflector.cs new file mode 100644 index 0000000..efc9d34 --- /dev/null +++ b/GodotHelper/src/Reflector.cs @@ -0,0 +1,126 @@ +namespace GodotHelpers; + +using System; +using System.Collections.Generic; +using System.Linq; +using Godot; +using Godot.Collections; +using Array = Godot.Collections.Array; +/// +/// Strong‑typed view of Godot's get_method_list / get_property_list output. +/// +public static class Reflector +{ + /* ──────────── Typed records ──────────── */ + + public readonly record struct ArgInfo( + string Name, + Variant.Type Type, + PropertyHint Hint, + string HintString, + Variant DefaultValue); + + public readonly record struct MethodInfoEx( + string Name, + IReadOnlyList Args, + IReadOnlyList DefaultArgs, + MethodFlags Flags, + int Id, + ArgInfo? ReturnValue); + + public readonly record struct PropertyInfoEx( + string Name, + string ClassName, + Variant.Type Type, + PropertyHint Hint, + string HintString, + PropertyUsageFlags Usage); + + /* ──────────── Public helpers ──────────── */ + + public static List GetMethodsListEx(this GodotObject godotObject) => GetMethods(godotObject); + public static List GetMethods(GodotObject obj) + { + var raw = obj.GetMethodList(); + var list = new List(raw.Count); + + foreach (Dictionary dict in raw) + { + // — Parse args — + var argsRaw = (Array)dict["args"]; + var args = new List(argsRaw.Count); + foreach (Dictionary a in argsRaw) + args.Add(ParseArg(a)); + + // — Parse return (may be empty) — + ArgInfo? ret = null; + if (dict.TryGetValue("return", out var retRaw) && retRaw.AsGodotDictionary() is Dictionary rd && rd.Count > 0) + ret = ParseArg(rd); + + list.Add(new MethodInfoEx( + Name: (string)dict["name"], + Args: args, + DefaultArgs: (Array)dict["default_args"], + Flags: (MethodFlags)(int)dict["flags"], + Id: (int)dict["id"], + ReturnValue: ret + )); + } + return list; + } + public static bool TryGetMethodInfo(this GodotObject godotObject, string property, out MethodInfoEx info) + { + foreach (var item in GetMethods(godotObject)) + { + if (item.Name == property) + { + info = item; + return true; + } + } + info = default; + return false; + } + public static List GetPropertyListEx(this GodotObject godotObject) => GetProperties(godotObject); + public static List GetProperties(GodotObject obj) + { + var raw = obj.GetPropertyList(); + var list = new List(raw.Count); + + foreach (Dictionary dict in raw) + { + list.Add(new PropertyInfoEx( + Name: (string)dict["name"], + ClassName: (string)dict["class_name"], + Type: (Variant.Type)(int)dict["type"], + Hint: (PropertyHint)(int)dict["hint"], + HintString: (string)dict["hint_string"], + Usage: (PropertyUsageFlags)(int)dict["usage"] + )); + } + return list; + } + public static PropertyInfoEx GetProperty(this IEnumerable properties, string name) => properties.FirstOrDefault(item => item.Name == name); + public static bool TryGetPropertyInfo(this GodotObject godotObject, string property, out PropertyInfoEx info) + { + foreach (var item in GetProperties(godotObject)) + { + if (item.Name == property) + { + info = item; + return true; + } + } + info = default; + return false; + } + /* ──────────── Internals ──────────── */ + + private static ArgInfo ParseArg(Dictionary d) => new( + Name: (string)d["name"], + Type: (Variant.Type)(int)d["type"], + Hint: (PropertyHint)(int)d["hint"], + HintString: (string)d["hint_string"], + DefaultValue: d.TryGetValue("default_value", out var dv) ? dv : default); + +} diff --git a/GodotHelper/src/VariantUtils.cs b/GodotHelper/src/VariantUtils.cs new file mode 100644 index 0000000..cf7edaf --- /dev/null +++ b/GodotHelper/src/VariantUtils.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using Godot; +using Godot.Collections; +namespace SJK.GodotHelpers; + +public static class VariantUtils +{ + public static Variant SafeToVariant(object value) + { + return value switch + { + null => new Variant(), + bool b => Variant.From(b), + int i => Variant.From(i), + long l => Variant.From((int)l), // Godot Variant only supports 32-bit int + float f => Variant.From(f), + double d => Variant.From((float)d), + string s => Variant.From(s), + Vector2 v2 => Variant.From(v2), + Vector2I v2i => Variant.From(v2i), + Vector3 v3 => Variant.From(v3), + Vector3I v3i => Variant.From(v3i), + Vector4 v4 => Variant.From(v4), + Vector4I v4i => Variant.From(v4i), + Rect2 rect2 => Variant.From(rect2), + Rect2I rect2i => Variant.From(rect2i), + Quaternion q => Variant.From(q), + Basis basis => Variant.From(basis), + Transform2D t2d => Variant.From(t2d), + Transform3D t3d => Variant.From(t3d), + Color color => Variant.From(color), + Plane plane => Variant.From(plane), + Aabb aabb => Variant.From(aabb), + GodotObject go => Variant.From(go), + byte[] bytes => Variant.From(bytes), + StringName sn => Variant.From(sn), + NodePath np => Variant.From(np), + Callable call => Variant.From(call), + Signal sig => Variant.From(sig), + Dictionary dict => Variant.From(dict), + Godot.Collections.Array array => Variant.From(array), + _ => throw new InvalidCastException($"Unsupported type '{value?.GetType().FullName}' for Variant conversion.") + }; + } + public static Type GetSystemType(this Variant.Type type) => type switch + { + Variant.Type.Nil => typeof(object), + Variant.Type.Bool => typeof(bool), + Variant.Type.Int => typeof(int), + Variant.Type.Float => typeof(float), + Variant.Type.String => typeof(string), + Variant.Type.Vector2 => typeof(Vector2), + Variant.Type.Vector2I => typeof(Vector2I), + Variant.Type.Rect2 => typeof(Rect2), + Variant.Type.Rect2I => typeof(Rect2I), + Variant.Type.Vector3 => typeof(Vector3), + Variant.Type.Vector3I => typeof(Vector3I), + Variant.Type.Vector4 => typeof(Vector4), + Variant.Type.Vector4I => typeof(Vector4I), + Variant.Type.Transform2D => typeof(Transform2D), + Variant.Type.Transform3D => typeof(Transform3D), + Variant.Type.Basis => typeof(Basis), + Variant.Type.Quaternion => typeof(Quaternion), + Variant.Type.Aabb => typeof(Aabb), + Variant.Type.Color => typeof(Color), + Variant.Type.Plane => typeof(Plane), + Variant.Type.StringName => typeof(StringName), + Variant.Type.NodePath => typeof(NodePath), + Variant.Type.Rid => typeof(Rid), + Variant.Type.Object => typeof(GodotObject), + Variant.Type.Callable => typeof(Callable), + Variant.Type.Signal => typeof(Signal), + Variant.Type.Dictionary => typeof(Dictionary), + Variant.Type.Array => typeof(Godot.Collections.Array), + _ => typeof(object) + }; +} diff --git a/global.json b/global.json index f42c6a3..07b2de6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.125", + "version": "10.0.104", "rollForward": "latestMinor" }, "msbuild-sdks": {