Skip to content

Commit

Permalink
Added @signal macro
Browse files Browse the repository at this point in the history
  • Loading branch information
samdeane committed Dec 19, 2024
1 parent 457a15c commit 18cf597
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 160 deletions.
12 changes: 6 additions & 6 deletions Generator/Generator/ClassGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,10 @@ func generateSignals (_ p: Printer,
if signal.arguments != nil {
parameterSignals.append (signal)

signalProxyType = getGenericSignalType(signal)
lambdaSig = " \(getGenericSignalLambdaArgs(signal)) in"
signalProxyType = getSignalType(signal)
lambdaSig = " \(getSignalLambdaArgs(signal)) in"
} else {
signalProxyType = "GenericSignal< /* no args */ >"
signalProxyType = "SimpleSignal"
lambdaSig = ""
}
let signalName = godotMethodToSwift (signal.name)
Expand All @@ -511,7 +511,7 @@ func generateSignals (_ p: Printer,
}

/// Return the type of a signal's parameters.
func getGenericSignalType(_ signal: JGodotSignal) -> String {
func getSignalType(_ signal: JGodotSignal) -> String {
var argTypes: [String] = []
for signalArgument in signal.arguments ?? [] {
let godotType = getGodotType(signalArgument)
Expand All @@ -524,12 +524,12 @@ func getGenericSignalType(_ signal: JGodotSignal) -> String {
}
}

return argTypes.isEmpty ? "GenericSignal< /* no args */ >" : "GenericSignal<\(argTypes.joined(separator: ", "))>"
return argTypes.isEmpty ? "SimpleSignal" : "SignalWithArguments<\(argTypes.joined(separator: ", "))>"
}

/// Return the names of a signal's parameters,
/// for use in documenting the corresponding lambda.
func getGenericSignalLambdaArgs(_ signal: JGodotSignal) -> String {
func getSignalLambdaArgs(_ signal: JGodotSignal) -> String {
var argNames: [String] = []
for signalArgument in signal.arguments ?? [] {
argNames.append(escapeSwift(snakeToCamel(signalArgument.name)))
Expand Down
File renamed without changes.
18 changes: 9 additions & 9 deletions Sources/SwiftGodot/Core/SignalRegistration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWithNoArguments {
public let name: StringName
public let arguments: [PropInfo] = [] // needed for registration in macro, but always []
Expand All @@ -17,7 +17,7 @@ public struct SignalWithNoArguments {
}

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWith1Argument<Argument: VariantStorable> {
public let name: StringName
public let arguments: [PropInfo]
Expand All @@ -34,7 +34,7 @@ public struct SignalWith1Argument<Argument: VariantStorable> {
}

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWith2Arguments<
Argument1: VariantStorable,
Argument2: VariantStorable
Expand All @@ -56,7 +56,7 @@ public struct SignalWith2Arguments<
}

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWith3Arguments<
Argument1: VariantStorable,
Argument2: VariantStorable,
Expand All @@ -81,7 +81,7 @@ public struct SignalWith3Arguments<
}

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWith4Arguments<
Argument1: VariantStorable,
Argument2: VariantStorable,
Expand Down Expand Up @@ -109,7 +109,7 @@ public struct SignalWith4Arguments<
}

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWith5Arguments<
Argument1: VariantStorable,
Argument2: VariantStorable,
Expand Down Expand Up @@ -140,7 +140,7 @@ public struct SignalWith5Arguments<
}

/// Describes a signal and its arguments.
/// - note: It is recommended to use the #signal macro instead of using this directly.
/// - note: It is recommended to use the @Signal macro instead of using this directly.
public struct SignalWith6Arguments<
Argument1: VariantStorable,
Argument2: VariantStorable,
Expand Down Expand Up @@ -375,8 +375,8 @@ public extension Object {
}
}

private extension PropInfo {
init(
extension PropInfo {
fileprivate init(
propertyType: (some VariantStorable).Type,
propertyName: StringName
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,44 @@
// Created by Sam Deane on 25/10/2024.
//

/// Simple signal without arguments.
public typealias SimpleSignal = SignalWithArguments< /* no args */>

/// Signal support.
/// Use the ``GenericSignal/connect(flags:_:)`` method to connect to the signal on the container object,
/// and ``GenericSignal/disconnect(_:)`` to drop the connection.
///
/// Use the ``GenericSignal/emit(...)`` method to emit a signal.
///
/// You can also await the ``Signal1/emitted`` property for waiting for a single emission of the signal.
/// Use the ``connect(flags:_:)`` method to connect to the signal on the container object,
/// and ``disconnect(_:)`` to drop the connection.
/// Use the ``emit(...)`` method to emit a signal.
/// You can also await the ``emitted`` property for waiting for a single emission of the signal.
///
public class GenericSignal<each T: VariantStorable> {
public class SignalWithArguments<each T: VariantStorable> {
var target: Object
var signalName: StringName
public init(target: Object, signalName: StringName) {
public init(target: Object, signalName: String) {
self.target = target
self.signalName = signalName
self.signalName = StringName(signalName)
}

/// Register this signal with the Godot runtime.
// TODO: the @Signal macro could optionally accept a list of argument names, so that we could register them as well.
public static func register<C: Object>(_ signalName: String, info: ClassInfo<C>) {
let arguments = expandArguments(repeat (each T).self)
info.registerSignal(name: StringName(signalName), arguments: arguments)
}

/// Expand a list of argument types into a list of PropInfo objects
/// Note: it doesn't currently seem to be possible to constrain
/// the type of the pack expansion to be ``VariantStorable.Type``, but
/// we know that it always will be, so we can force cast it.
static func expandArguments<each ArgType>(_ type: repeat each ArgType) -> [PropInfo] {
var args = [PropInfo]()
var argC = 1
for arg in repeat each type {
let a = arg as! any VariantStorable.Type
args.append(a.propInfo(name: "arg\(argC)"))
argC += 1

}
return args
}

/// Connects the signal to the specified callback
Expand All @@ -26,7 +50,7 @@ public class GenericSignal<each T: VariantStorable> {
/// - flags: Optional, can be also added to configure the connection's behavior (see ``Object/ConnectFlags`` constants).
/// - Returns: an object token that can be used to disconnect the object from the target on success, or the error produced by Godot.
///
@discardableResult /* Signal1 */
@discardableResult
public func connect(flags: Object.ConnectFlags = [], _ callback: @escaping (_ t: repeat each T) -> Void) -> Object {
let signalProxy = SignalProxy()
signalProxy.proxy = { args in
Expand All @@ -50,6 +74,31 @@ public class GenericSignal<each T: VariantStorable> {
target.disconnect(signal: signalName, callable: Callable(object: token, method: SignalProxy.proxyName))
}

/// Emit the signal (with required arguments, if there are any)
@discardableResult /* discardable per discardableList: Object, emit_signal */
public func emit(_ t: repeat each T) -> GodotError {
// NOTE:
// Ideally we should be able to expand the arguments and pass them
// into a call to the native emitSignal; something like this:
// emitSignal(signalName, repeat Variant(each t))
//
// Unfortunately, expanding arguments as opposed to types
// (t, as opposed to T), doesn't seem to support this pattern.
//
// The only thing we can do with them is iterate them,
// which means that we can build up an array of them, so we
// then use callv to call the emit_signal method.
let args = GArray()
args.append(Variant(signalName))
for arg in repeat each t {
args.append(Variant(arg))
}
let result = target.callv(method: "emit_signal", argArray: args)
guard let result else { return .ok }
guard let errorCode = Int(result) else { return .ok }
return GodotError(rawValue: Int64(errorCode))!
}

/// You can await this property to wait for the signal to be emitted once.
public var emitted: Void {
get async {
Expand All @@ -71,43 +120,43 @@ extension Arguments {
enum UnpackError: Error {
/// The argument could not be coerced to the expected type.
case typeMismatch

/// The argument was nil.
case nilArgument
}

/// Unpack an argument as a specific type.
/// We throw a runtime error if the argument is not of the expected type,
/// or if there are not enough arguments to unpack.
func unwrap<T: VariantStorable>(ofType type: T.Type, index: inout Int) throws -> T {
let argument = try optionalVariantArgument(at: index)
index += 1

// if the argument was nil, throw error
guard let argument else {
throw UnpackError.nilArgument
}

// NOTE:
// Ideally we could just call T.unpack(from: argument) here.
// Unfortunately, T.unpack is dispatched statically, but we don't
// have the full dynamic type information for T when we're compiling.
// The only thing we know about type T is that it conforms to VariantStorable.
// We don't know if inherits from Object, so the compiler will always pick the
// default non-object implementation of T.unpack.

// try to unpack the variant as the expected type
let value: T?
if (argument.gtype == .object) && (T.Representable.godotType == .object) {
value = argument.asObject(Object.self) as? T
} else {
value = T(argument)
}

guard let value else {
throw UnpackError.typeMismatch
}

return value
}
}
16 changes: 16 additions & 0 deletions Sources/SwiftGodot/Core/VariantStorable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ extension VariantStorable {
}
}


extension VariantStorable {
/// Return PropInfo for this storage type.
static func propInfo(name: String) -> PropInfo {
let gType = Self.Representable.godotType
return PropInfo(
propertyType: gType,
propertyName: StringName(name),
className: gType == .object ? StringName(String(describing: Self.self)) : "",
hint: .none,
hintStr: "",
usage: .default
)
}
}

extension String: VariantStorable {
public func toVariantRepresentable() -> GString {
let r = GString()
Expand Down
21 changes: 21 additions & 0 deletions Sources/SwiftGodot/MacroDefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,25 @@ public macro SceneTree(path: String? = nil) = #externalMacro(module: "SwiftGodot
@freestanding(declaration, names: arbitrary)
public macro signal(_ signalName: String, arguments: Dictionary<String, Any.Type> = [:]) = #externalMacro(module: "SwiftGodotMacroLibrary", type: "SignalMacro")

/// Defines a Godot signal on a class.
///
/// The `@Godot` macro will register any #signal defined signals so that they can be used in the editor.
///
/// Usage:
/// ```swift
/// @Godot class MyNode: Node2D {
/// @Signal var gameStarted: SimpleSignal
/// @Signal var livesChanged: SignalWithArguments<Int>
///
/// func startGame() {
/// gameStarted.emit()
/// livesChanged.emit(5)
/// }
/// }
/// ```

@attached(accessor)
public macro Signal() = #externalMacro(module: "SwiftGodotMacroLibrary", type: "SignalAttachmentMacro")


#endif
2 changes: 1 addition & 1 deletion Sources/SwiftGodot/SwiftGodot.docc/CustomTypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class SwiftSprite: Sprite2D {
```

When you use the `@Godot` macro, a number of additional macros can be used inside
your class, like `#signal` [to define signals](Signals.md), `@Callable` to surface a method to
your class, like `@Signal` [to define signals](Signals.md), `@Callable` to surface a method to
Godot, and `@Export` to [surface properties](Exports.md).

Behind the scenes these macros use the lower-level ``ClassDB`` API to define functions,
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftGodot/SwiftGodot.docc/Differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ for additional information.

## Signals

Generally, use the `#signal` macro to declare a new signal for your classes,
Generally, use the `@Signal` macro to declare a new signal for your classes,
like this:

```swift
class Demo: Node {
#signal("lives_changed", argument: ["new_lives_count": Int.self])
@Signal var livesChanged: SignalWithArguments<Int>
}
```

Expand Down
Loading

0 comments on commit 18cf597

Please sign in to comment.