tirbofish/dropbear · diff
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)
Signature present but could not be verified.
Unverified
@@ -27,4 +27,5 @@ docs dropbear_future-queue/test.log */.build/* -.build/* +.build/* +*.resolved @@ -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", @@ -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() } @@ -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)") } } @@ -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) } @@ -1,4 +0,0 @@ -class ScriptRegistry { - var scripts: [String: () -> RunnableScript] = [:] - -} @@ -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 + ] +}