diff --git a/Sources/SwiftGodot/Extensions/ClassInfo+ConvenienceProperties.swift b/Sources/SwiftGodot/Extensions/ClassInfo+ConvenienceProperties.swift new file mode 100644 index 000000000..dd0443fe2 --- /dev/null +++ b/Sources/SwiftGodot/Extensions/ClassInfo+ConvenienceProperties.swift @@ -0,0 +1,263 @@ +// +// ClassInfo+ConvenienceProperties.swift +// +// +// Created by Marquis Kurt on 5/29/23. +// + +import Foundation +import UniformTypeIdentifiers + +public protocol Nameable { + var name: String { get } +} + +@available(macOS 11.0, *) +extension UTType { + /// The file type that corresponds to a Godot resource file. + public static var godotResource = UTType(filenameExtension: "res") + + /// The file type that corresponds to a Godot scene file. + public static var godotScene = UTType(filenameExtension: "tscn") + + /// The file type that corresponds to a GDScript file. + public static var gdscript = UTType(filenameExtension: "gd") + + /// The file type that corresponds to a Godot shader file. + public static var godotShader = UTType(filenameExtension: "gdshader") + + /// The file type that corresponds to a Godot tileset resource. + public static var godotTilesetResource = UTType(filenameExtension: "tres") +} + +extension ClassInfo { + /// A type alias referencing a class info function that can be registered. + public typealias ClassInfoFunction = (T) -> ([Variant]) -> Variant? + + /// A type alias referencing a registerable int enum. + public typealias RegisteredIntEnum = CaseIterable & Nameable & RawRepresentable + + /// Performs an operation on an argument that originates from a setter method. + /// + /// This can be used inside setter method to set a property in-class with a guaranteed argument value. + /// + /// ```swift + /// func setBubbleCount(args: [Variant]) -> Variant? { + /// withCheckedProperty(named: "bubbles", in: args) { argument in + /// self.bubbles = Int(argument) ?? 0 + /// } + /// } + /// ``` + /// + /// - Parameter name: The name of the property that is being set. + /// - Parameter arguments: The list of arguments that were passed from the setter. + /// - Parameter action: A closure that accepts a valid argument. + public static func withCheckedProperty(named name: String, + in arguments: [Variant], + perform action: (Variant) -> Void) -> Variant? { + guard let arg = arguments.first else { + GD.pushError("Expected argument for \(name), but got nil instead.") + return nil + } + action(arg) + return nil + } + + /// Registers a checkbox toggle in the editor. + /// - Parameter name: The name of the property that will appear in the editor. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + public func registerCheckbox(named name: String, + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let property = PropInfo(propertyType: .bool, + propertyName: StringName("\(registeredPrefix)_\(name)"), + className: StringName("\(T.self)"), + hint: .flags, + hintStr: "", + usage: .propertyUsageDefault) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + /// Registers an enumeration in the editor. + /// - Parameter name: The name of the property that will appear in the editor. + /// - Parameter enumType: The enumeration type that will be selected in the editor. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + public func registerEnum(named name: String, + for enumType: Enum.Type, + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let property = PropInfo(propertyType: .int, + propertyName: StringName("\(registeredPrefix)_\(name)"), + className: StringName("\(T.self)"), + hint: .enum, + hintStr: GString(Enum.allCases.map(\.name).joined(separator: ",")), + usage: .propertyUsageDefault) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + /// Registers a file picker in the editor. + /// + /// - Important: This method is deprecated on macOS 12.0 or later; Use the + /// ``ClassInfo/registerFilePicker(named:allowedTypes:prefix:getter:setter:)-9oeps`` method for newer macOS + /// versions. + /// + /// - Parameter name: The name of the property that will appear in the editor. + /// - Parameter allowedTypes: The types of files allowed in the file picker. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + @available(macOS, introduced: 10.15, deprecated: 12.0, message: "Use the variant with UTTypes.") + public func registerFilePicker(named name: String, + allowedTypes: [String] = ["*"], + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let fileExtensions = allowedTypes.map { "*.\($0)" }.joined(separator: ",") + let property = PropInfo(propertyType: .string, + propertyName: StringName(name), + className: StringName("\(T.self)"), + hint: .file, + hintStr: GString(fileExtensions), + usage: .propertyUsageDefault) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + /// Registers a file picker in the editor. + /// - Parameter name: The name of the property that will appear in the editor. + /// - Parameter allowedTypes: The types of files allowed in the file picker. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + @available(macOS 11.0, *) + public func registerFilePicker(named name: String, + allowedTypes: [UTType], + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let fileExtensions = allowedTypes.map(\.preferredFilenameExtension) + .map { "*.\($0 ?? "*")" } + .joined(separator: ",") + let property = PropInfo(propertyType: .string, + propertyName: StringName(name), + className: StringName("\(T.self)"), + hint: .file, + hintStr: GString(fileExtensions), + usage: .propertyUsageDefault) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + /// Registers a number field in the number that can be adjusted in a range. + /// - Parameter string: The name of the property that will appear in the editor. + /// - Parameter range: The range that the number must fall between. + /// - Parameter stride: The number to decrease or increase by. Defaults to 1. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + public func registerInt(named name: String, + range: ClosedRange, + stride: Int = 1, + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let property = PropInfo(propertyType: .int, + propertyName: StringName("\(registeredPrefix)_\(name)"), + className: StringName("\(T.self)"), + hint: .range, + hintStr: GString("\(range.lowerBound),\(range.upperBound),\(stride)"), + usage: .propertyUsageDefault) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + /// Registers a text field. + /// - Parameter name: The name of the property that will appear in the editor. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + public func registerTextField(named name: String, + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let property = PropInfo(propertyType: .string, + propertyName: StringName("\(registeredPrefix)_\(name)"), + className: StringName("\(T.self)"), + hint: .typeString, + hintStr: "", + usage: .propertyUsageDefault) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + /// Registers a multiline text view in the editor. + /// - Parameter name: The name of the property that will appear in the editor. + /// - Parameter prefix: The prefix to apply to the property name. Defaults to the class's name if not provided. + /// - Parameter getter: The getter method the editor will call to get the property. + /// - Parameter setter: The setter method the editor will call to set the property. + public func regsiterTextView(named name: String, + prefix: String? = nil, + getter: @escaping ClassInfoFunction, + setter: @escaping ClassInfoFunction) { + let registeredPrefix = prefix ?? "\(T.self)" + let property = PropInfo(propertyType: .string, + propertyName: StringName("\(registeredPrefix)_\(name)"), + className: StringName("\(T.self)"), + hint: .multilineText, + hintStr: "", + usage: .propertyUsageDefault) + registerSetter(prefix: registeredPrefix, name: name, property: property, setter: setter) + registerGetter(prefix: registeredPrefix, name: name, property: property, getter: getter) + registerProperty(property, + getter: StringName("\(registeredPrefix)_get_\(name)"), + setter: StringName("\(registeredPrefix)_set_\(name)")) + } + + private func registerGetter(prefix: String, name: String, property: PropInfo, getter: @escaping ClassInfoFunction) { + registerMethod(name: StringName("\(prefix)_get_\(name)"), + flags: .default, + returnValue: property, + arguments: [], + function: getter) + } + + private func registerSetter(prefix: String, name: String, property: PropInfo, setter: @escaping ClassInfoFunction) { + registerMethod(name: StringName("\(prefix)_set_\(name)"), + flags: .default, + returnValue: nil, + arguments: [property], + function: setter) + } +} diff --git a/scripts/make-swiftgodot-framework b/scripts/make-swiftgodot-framework index c6f1c5309..dbf284147 100755 --- a/scripts/make-swiftgodot-framework +++ b/scripts/make-swiftgodot-framework @@ -20,24 +20,34 @@ case $2 in ;; esac +arch=`uname -m` + rm -rf $2 bdMac=$1/Build/Products/Debug/ bdMacArm="$(echo "$1" | rev | cut -c2- | rev)_arm/Build/Products/Debug/" bdiOS=$1/Build/Products/Debug-iphoneos/ mkdir universal -echo $bdMacArm -cp -rf $bdMacArm/PackageFrameworks/SwiftGodot.framework universal/ -lipo -create -output ./universal/SwiftGodot.framework/SwiftGodot $bdMac/PackageFrameworks/SwiftGodot.framework/SwiftGodot $bdMacArm/PackageFrameworks/SwiftGodot.framework/SwiftGodot +cp -rf $bdMac/PackageFrameworks/SwiftGodot.framework universal/ +if [ $arch = "arm64" ]; then + lipo -create -output ./universal/SwiftGodot.framework/SwiftGodot $bdMac/PackageFrameworks/SwiftGodot.framework/SwiftGodot $bdMacArm/PackageFrameworks/SwiftGodot.framework/SwiftGodot + lipo -create -output ./universal/SwiftGodot.framework/Versions/A/SwiftGodot $bdMac/PackageFrameworks/SwiftGodot.framework/Versions/A/SwiftGodot $bdMacArm/PackageFrameworks/SwiftGodot.framework/Versions/A/SwiftGodot +fi xcodebuild -create-xcframework -framework ./universal/SwiftGodot.framework -framework $bdiOS/PackageFrameworks/SwiftGodot.framework -output $2 rm -rf ./universal -fd=$2//macos-arm64_x86_64/SwiftGodot.framework +if [ $arch = "arm64" ]; then + fd=$2//macos-arm64_x86_64/SwiftGodot.framework +else + fd=$2/macos-x86_64/SwiftGodot.framework +fi ifd=$2/ios-arm64/SwiftGodot.framework (cd $fd; mkdir -p Versions/Current/Headers; ln -sf Versions/Current/Headers .) (cd $fd; mkdir -p Versions/Current/Modules; ln -sf Versions/Current/Modules .) (cd $ifd; mkdir -p Versions/Current/Headers; ln -sf Versions/Current/Headers .) (cd $ifd; mkdir -p Versions/Current/Modules; ln -sf Versions/Current/Modules .) rsync -a $bdMac/SwiftGodot.swiftmodule $fd/Versions/Current/Modules/ -rsync -a $bdMacArm/SwiftGodot.swiftmodule $fd/Versions/Current/Modules/ +if [ $arch = "arm64" ]; then + rsync -a $bdMacArm/SwiftGodot.swiftmodule $fd/Versions/Current/Modules/ +fi rsync -a $bdiOS/SwiftGodot.swiftmodule $ifd/Versions/Current/Modules/ cat > $fd/Modules/module.modulemap << EOF diff --git a/scripts/release b/scripts/release index 7123f1bdd..dfec694a0 100644 --- a/scripts/release +++ b/scripts/release @@ -32,6 +32,8 @@ if [ -z "$SWIFT_GODOT_NODEPLOY" ]; then esac fi +arch=`uname -m` + outputDir=$HOME/sg-builds dir=$outputDir/$$-build @@ -45,8 +47,10 @@ echo DerivedData: $derivedData echo ArchiveData: $archivePath start=`date` xcodebuild -scheme SwiftGodot -destination platform=macOS,arch=x86_64 -derivedDataPath $derivedData -archivePath $archivePath >& $dir/x86_64.log -xcodebuild -scheme SwiftGodot -destination platform=macOS,arch=arm64 -derivedDataPath "${derivedData}_arm" -archivePath $archivePath >& $dir/arm64.log -xcodebuild -scheme SwiftGodot -destination generic/platform=iOS -derivedDataPath $derivedData -archivePath $archivePath >& $dir/arm64.log +if [ $arch = "arm64" ]; then + xcodebuild -scheme SwiftGodot -destination platform=macOS,arch=arm64 -derivedDataPath "${derivedData}_arm" -archivePath $archivePath >& $dir/arm64.log +fi +xcodebuild -scheme SwiftGodot -destination generic/platform=iOS -derivedDataPath $derivedData -archivePath $archivePath >& $dir/ios_arm64.log sh scripts/make-swiftgodot-framework $dir/derived/ $outputDir/SwiftGodot.xcframework if [ ! -z "$SWIFT_GODOT_NODEPLOY" ]; then