kitgit

tirbofish/dropbear · diff

83e2d41 · tk

helper macros for the swift lang such as @Script(name) and @ScriptEntry (i also realised that i don't have a good enough build so i will run jarvis, run github actions)

Unverified

diff --git a/.gitignore b/.gitignore
index dc8b8b9..ec61ccd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,5 @@ docs
 dropbear_future-queue/test.log
 
 */.build/*
-.build/*
\ No newline at end of file
+.build/*
+*.resolved
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
index e5ab6f8..ae52bc3 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,6 +1,7 @@
 // swift-tools-version: 6.2
 
 import PackageDescription
+import CompilerPluginSupport
 
 let package = Package(
     name: "dropbear",
@@ -12,11 +13,19 @@ let package = Package(
         ),
     ],
     dependencies: [
-        // .package(url: "https://github.com/nicklockwood/VectorMath", from: "0.4.0")
+        .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0")
     ],
     targets: [
+        .macro(
+            name: "dropbear_macro",
+            dependencies: [
+                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
+                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
+            ]
+        ),
         .target(
-            name: "dropbear"
+            name: "dropbear",
+            dependencies: ["dropbear_macro"]
         ),
         .testTarget(
             name: "dropbearTests",
diff --git a/Sources/dropbear/dropbear.swift b/Sources/dropbear/dropbear.swift
index 24a86be..97c09d4 100644
--- a/Sources/dropbear/dropbear.swift
+++ b/Sources/dropbear/dropbear.swift
@@ -1,19 +1,116 @@
 /// A class that all scripts inherit, which define simple functions which are ran
 /// in the dropbear rust engine. 
-public class RunnableScript {
+public protocol RunnableScript: AnyObject {
+    /// The init function. This is the constructor for the class to initialise. 
+    /// 
+    /// You can setup whatever variables and states, however it is recommended to use
+    /// the `onLoad` function instead as that is actually ran during the runtime
+    /// compared to `init`, which is initialised when the library is loaded. 
+    init()
+
     /// Loads the content/variables/states into the entity/scene. 
-    /// It runs ONLY ONCE. 
-    public func onLoad() {
-        fatalError("Hey there! I'm not overriden. You might wanna do something about this onLoad function!")
-    }
+    /// 
+    /// This is ran only once and is loaded during runtime. 
+    func onLoad()
 
     /// Updates the engine context with the content inside the function
+    /// 
+    /// This is ran every frame and is loaded during the runtime. 
     /// - Parameter dt: Deltatime/the time it takes for the previous frame to render as a `Float`
-    public func onUpdate(dt: Float) {
-        fatalError("Hey there! I'm not overriden. You might wanna do something about this onUpdate function!")
+    func onUpdate(dt: Float)
+
+    /// Run the script (called by engine)
+    func run()
+}
+
+/// Registry for managing script classes and their metadata. 
+@MainActor
+public class ScriptRegistry {
+    private static var scriptClasses: [String: RunnableScript.Type] = [:]
+    private static var entityScripts: [String: String] = [:]  // entity -> fileName mapping
+    
+    /// Register a script class with its file name
+    public static func registerScript<T: RunnableScript>(_ type: T.Type, fileName: String) {
+        scriptClasses[fileName] = type
+    }
+    
+    /// Register a script for a specific entity
+    public static func registerEntityScript(_ fileName: String, entity: String) {
+        entityScripts[entity] = fileName
+    }
+    
+    /// Get script class by file name
+    public static func getScript(fileName: String) -> RunnableScript.Type? {
+        return scriptClasses[fileName]
+    }
+    
+    /// Get script file name for specific entity
+    public static func getScriptForEntity(_ entity: String) -> String? {
+        return entityScripts[entity]
+    }
+    
+    /// Create script instance by file name
+    public static func createScript(fileName: String) -> RunnableScript? {
+        return getScript(fileName: fileName)?.init()
+    }
+    
+    /// Create script instance for entity (checks entity-specific first, then falls back to fileName)
+    public static func createScriptForEntity(_ entity: String, fallbackFileName: String) -> RunnableScript? {
+        if let specificFileName = getScriptForEntity(entity) {
+            return createScript(fileName: specificFileName)
+        }
+        return createScript(fileName: fallbackFileName)
+    }
+}
+
+/// Base implementation of RunnableScript with default run() behavior
+open class BaseScript: RunnableScript {
+    public required init() {}
+    
+    open func onLoad() {
+        // Override in subclasses
+    }
+    
+    open func onUpdate(dt: Float) {
+        // Override in subclasses
+    }
+    
+    public func run() {
+        onLoad()
+        // Engine will call onUpdate in its loop
     }
 }
 
+/// A macro for a class of a script that can be used with any entity (when added).  
+/// 
+/// Let's say that you have an entity of a player. You want to get movement for
+/// that Player. The Eucalyptus Editor only allows for one `Swift` file per entity. 
+/// To combat that, there is a macro called `@ScriptEntry`, which allows for that class
+/// to be ran (in no particular order) in tandem with the Player.
+/// 
+/// In the case that you want a script to be locked to **only** a specific entity,
+/// you can use the `@Script(name: /*Entity Label*/)` to lock that class to run only on
+/// that entity, improving production as you won't have to constantly rewrite scripts. 
+@attached(member, names: named(init))
+public macro ScriptEntry() = #externalMacro(module: "dropbear_macro", type: "ScriptEntryMacro")
+
+/// A macro for a class of a script that can be used by a **specific** entity. 
+/// 
+/// Imagine you have an enemy. You have a class that deals with movement, a class that deals with 
+/// health, but you want an Enemy specific class for its own system. This macro helps with dealing with 
+/// such an issue, allowing you to attach this script to other entities as well. 
+/// 
+/// This macro also gives the class a higher priority compared to the `@ScriptEntry` classes, allowing this
+/// script to run earlier than any ScriptEntry derived classes. 
+/// 
+/// FYI: This macro does not update if you change the label. If the label in editor is 
+/// different than what is provided, this class will not run for that entity. 
+/// 
+/// # Parameters
+/// - name: A String to the label of the entity set by you.  
+@attached(member, names: named(init))
+public macro Script(entity: String) = #externalMacro(module: "dropbear_macro", type: "ScriptMacro")
+
 public func getInput() -> Input {
     Input()
 }
diff --git a/Sources/dropbear/example.swift b/Sources/dropbear/example.swift
index 37441c2..db247b9 100644
--- a/Sources/dropbear/example.swift
+++ b/Sources/dropbear/example.swift
@@ -1,8 +1,9 @@
 // example testing module, do not use in production
 
-class Player: RunnableScript {
+@ScriptEntry
+class Player: BaseScript {
     override func onLoad() {
-        print("I have risen")
+        print("Player script loaded")
         // let current_scene = dropbear.getCurrentScene()
         // let player = current_scene?.getEntity("player")
         if dropbear.getInput().isKeyPressed(Key.W) {
@@ -11,6 +12,6 @@ class Player: RunnableScript {
     }
 
     override func onUpdate(dt: Float) {
-        print("I am currently awake")
+        print("Player is running: \(dt)")
     }
 }
\ No newline at end of file
diff --git a/Sources/dropbear/math.swift b/Sources/dropbear/math.swift
index ea72e6d..8038e44 100644
--- a/Sources/dropbear/math.swift
+++ b/Sources/dropbear/math.swift
@@ -21,7 +21,8 @@ enum Math {
         var z: T
 
         /// Initialises a new Vector3
-        /// - Parameters:
+        /// 
+        /// # Parameters:
         ///   - x: A value of type T
         ///   - y: A value of type T
         ///   - z: A value of type T
@@ -31,8 +32,8 @@ enum Math {
             self.z = z
         }
 
-        /// Creates a new Vector3 of type T with all values set to zero. 
-        /// - Returns: Vector3 of type T
+        /// Creates a new Vector3 of type `T` with all values set to zero. 
+        /// - Returns: Vector3 of type `T`
         static func zero() -> Vector3<T> {
             return Vector3(x: 0, y: 0, z: 0)
         }
diff --git a/Sources/dropbear/registry.swift b/Sources/dropbear/registry.swift
deleted file mode 100644
index 33f0777..0000000
--- a/Sources/dropbear/registry.swift
+++ /dev/null
@@ -1,4 +0,0 @@
-class ScriptRegistry {
-    var scripts: [String: () -> RunnableScript] = [:]
-    
-}
\ No newline at end of file
diff --git a/Sources/dropbear_macro/macros.swift b/Sources/dropbear_macro/macros.swift
new file mode 100644
index 0000000..627d952
--- /dev/null
+++ b/Sources/dropbear_macro/macros.swift
@@ -0,0 +1,130 @@
+import SwiftCompilerPlugin
+import SwiftSyntax
+import SwiftSyntaxBuilder
+import SwiftSyntaxMacros
+import Foundation
+
+/// Already defined in `dropbear/@ScriptEntry`.
+/// A macro for a class of a script that can be used with any entity (when added).  
+/// 
+/// Let's say that you have an entity of a player. You want to get movement for
+/// that Player. The Eucalyptus Editor only allows for one `Swift` file per entity. 
+/// To combat that, there is a macro called `@ScriptEntry`, which allows for that class
+/// to be ran (in no particular order) in tandem with the Player.
+/// 
+/// In the case that you want a script to be locked to **only** a specific entity,
+/// you can use the `@Script(name: /*Entity Label*/)` to lock that class to run only on
+/// that entity, improving production as you won't have to constantly rewrite scripts. 
+public struct ScriptEntryMacro: MemberMacro {
+    public static func expansion(
+        of node: AttributeSyntax,
+        providingMembersOf declaration: some DeclGroupSyntax,
+        conformingTo protocols: [TypeSyntax],
+        in context: some MacroExpansionContext
+    ) throws -> [DeclSyntax] {
+        guard declaration.is(ClassDeclSyntax.self) else {
+            throw MacroError.notAClass
+        }
+        
+        let fileName = extractFileName(from: context, node: node)
+        
+        return [
+            """
+            required init() {
+                super.init()
+                Task { @MainActor in
+                    ScriptRegistry.registerScript(Self.self, fileName: \(literal: fileName))
+                }
+            }
+            """
+        ]
+    }
+}
+
+/// Already defined in `dropbear/@Script(name: /*Entity Label*/)`.
+/// A macro for a class of a script that can be used by a **specific** entity. 
+/// 
+/// Imagine you have an enemy. You have a class that deals with movement, a class that deals with 
+/// health, but you want an Enemy specific class for its own system. This macro helps with dealing with 
+/// such an issue, allowing you to attach this script to other entities as well. 
+/// 
+/// This macro also gives the class a higher priority compared to the `@ScriptEntry` classes, allowing this
+/// script to run earlier than any ScriptEntry derived classes. 
+/// 
+/// FYI: This macro does not update if you change the label. If the label in editor is 
+/// different than what is provided, this class will not run for that entity. 
+/// 
+/// # Parameters
+/// - name: A String to the label of the entity set by you.  
+public struct ScriptMacro: MemberMacro {
+    public static func expansion(
+        of node: AttributeSyntax,
+        providingMembersOf declaration: some DeclGroupSyntax,
+        conformingTo protocols: [TypeSyntax],
+        in context: some MacroExpansionContext
+    ) throws -> [DeclSyntax] {
+        guard declaration.is(ClassDeclSyntax.self) else {
+            throw MacroError.notAClass
+        }
+        
+        guard case let .argumentList(args) = node.arguments,
+            let firstArg = args.first,
+            let labeledExpr = firstArg.expression.as(StringLiteralExprSyntax.self) else {
+            throw MacroError.invalidEntityArgument
+        }
+        
+        let entityName = labeledExpr.segments.compactMap { segment in
+            if case let .stringSegment(content) = segment {
+                return content.content.text
+            }
+            return nil
+        }.joined()
+        
+        let fileName = extractFileName(from: context, node: node)
+        
+        return [
+            """
+            required init() {
+                super.init()
+                Task { @MainActor in
+                    ScriptRegistry.registerScript(Self.self, fileName: \(literal: fileName))
+                    ScriptRegistry.registerEntityScript(\(literal: fileName), entity: \(literal: entityName))
+                }
+            }
+            """
+        ]
+    }
+}
+
+private func extractFileName(from context: some MacroExpansionContext, node: AttributeSyntax) -> String {
+    if let location = context.location(of: node, at: .afterLeadingTrivia, filePathMode: .fileID) {
+        let fileString = location.file.description
+        let components = fileString.components(separatedBy: "/")
+        if let lastComponent = components.last {
+            return lastComponent.replacingOccurrences(of: ".swift", with: "")
+        }
+    }
+    return "unknown"
+}
+
+enum MacroError: Error, CustomStringConvertible {
+    case notAClass
+    case invalidEntityArgument
+    
+    var description: String {
+        switch self {
+        case .notAClass:
+            return "Script macros can only be applied to classes"
+        case .invalidEntityArgument:
+            return "Script macro requires a valid entity string argument"
+        }
+    }
+}
+
+@main
+struct MacroPlugin: CompilerPlugin {
+    let providingMacros: [Macro.Type] = [
+        ScriptEntryMacro.self,
+        ScriptMacro.self
+    ]
+}
\ No newline at end of file