Skip to content

Commit 087b260

Browse files
Surface _validate_property method in Godot for allowing custom properties to be controlled (#675)
* Surface _validate_property method in Godot for allowing custom properties to be controlled * Oh so the return value means - copy the results back, as a way of altering the properties * Make PropInfo fields variables, so they can be changed * Blindly: what if we keep a reference to the original to update * Revert last changes, crashes and does not fix it * Ok, now with a test: _validateProperty works
1 parent e2bc9d3 commit 087b260

File tree

4 files changed

+97
-7
lines changed

4 files changed

+97
-7
lines changed

Sources/SwiftGodot/Core/ClassServices.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,17 @@ public class ClassInfo<T:Object> {
199199
/// The supported types are those that can be wrapped as a Godot Variant type.
200200
public struct PropInfo: CustomDebugStringConvertible {
201201
/// The type of the property being defined
202-
public let propertyType: Variant.GType
202+
public var propertyType: Variant.GType
203203
/// The name for the property
204-
public let propertyName: StringName
204+
public var propertyName: StringName
205205
/// The class name where this is defined
206-
public let className: StringName
206+
public var className: StringName
207207
/// Property Hint for this property
208-
public let hint: PropertyHint
208+
public var hint: PropertyHint
209209
/// Human-readable hint
210-
public let hintStr: GString
210+
public var hintStr: GString
211211
/// Describes how the property can be used.
212-
public let usage: PropertyUsageFlags
212+
public var usage: PropertyUsageFlags
213213

214214
public init(propertyType: Variant.GType, propertyName: StringName, className: StringName, hint: PropertyHint, hintStr: GString, usage: PropertyUsageFlags) {
215215
self.propertyType = propertyType

Sources/SwiftGodot/Core/Wrapped.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,12 @@ open class Wrapped: Equatable, Identifiable, Hashable {
201201
/// For example `Node.notificationProcess`
202202
open func _notification(code: Int, reversed: Bool) {
203203
}
204-
204+
205+
/// Called whenever Godot retrieves value of property. Allows to customize existing properties.
206+
/// Return true if you made changes to the PropInfo value you got
207+
open func _validateProperty(_ property: inout PropInfo) -> Bool {
208+
return false
209+
}
205210

206211
/// Checks if this object has a script with the given method.
207212
/// - Parameter method: StringName identifying the method.
@@ -352,6 +357,7 @@ func register<T:Wrapped> (type name: StringName, parent: StringName, type: T.Typ
352357
info.get_virtual_func = getVirtual
353358
info.notification_func = notificationFunc
354359
info.recreate_instance_func = recreateFunc
360+
info.validate_property_func = validatePropertyFunc
355361
info.is_exposed = 1
356362
userTypes [name.description] = { ptr in
357363
return type.init(nativeHandle: ptr)
@@ -547,6 +553,38 @@ func notificationFunc (ptr: UnsafeMutableRawPointer?, code: Int32, reversed: UIn
547553
original._notification(code: Int(code), reversed: reversed != 0)
548554
}
549555

556+
func validatePropertyFunc(ptr: UnsafeMutableRawPointer?, _info: UnsafeMutablePointer<GDExtensionPropertyInfo>?) -> UInt8 {
557+
guard let ptr else { return 0 }
558+
let original = Unmanaged<Wrapped>.fromOpaque(ptr).takeUnretainedValue()
559+
guard var info = _info?.pointee else { return 0 }
560+
guard let namePtr = info.name,
561+
let classNamePtr = info.class_name,
562+
let infoHintPtr = info.hint_string else {
563+
return 0
564+
}
565+
guard let ptype = Variant.GType(rawValue: Int64(info.type.rawValue)) else { return 0 }
566+
let pname = StringName(fromPtr: namePtr)
567+
let className = StringName(fromPtr: classNamePtr)
568+
let hint = PropertyHint(rawValue: Int64(info.hint)) ?? .none
569+
let hintStr = GString(content: infoHintPtr.load(as: Int64.self))
570+
let usage = PropertyUsageFlags(rawValue: Int(info.usage))
571+
572+
var pinfo = PropInfo(propertyType: ptype, propertyName: pname, className: className, hint: hint, hintStr: hintStr, usage: usage)
573+
if original._validateProperty(&pinfo) {
574+
// The problem with the code below is that it does not make a copy of the StringName and String,
575+
// and passes a reference that we will destroy right away when `pinfo` goes out of scope.
576+
//
577+
// For now, we just update the usage, type and hint but we need to find a solution for those other fields
578+
let native = pinfo.makeNativeStruct()
579+
_info?.pointee.usage = UInt32(pinfo.usage.rawValue)
580+
_info?.pointee.hint = UInt32(pinfo.hint.rawValue)
581+
_info?.pointee.type = GDExtensionVariantType(GDExtensionVariantType.RawValue (pinfo.propertyType.rawValue))
582+
583+
return 1
584+
}
585+
return 0
586+
}
587+
550588
func userTypeBindingCreate (_ token: UnsafeMutableRawPointer?, _ instance: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? {
551589
// Godot-cpp does nothing for user types
552590
//print ("SWIFT: instanceBindingCreate")

Tests/SwiftGodotTests/SignalTests.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ private class TestSignalNode: Node {
1313
receivedInt = age
1414
receivedString = name
1515
}
16+
17+
override func _validateProperty(_ prop: inout PropInfo) -> Bool {
18+
19+
return true
20+
}
1621
}
1722
1823
final class SignalTests: GodotTestCase {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// ValidatePropertyTests.swift
3+
// SwiftGodot
4+
//
5+
// Created by Miguel de Icaza on 3/29/25.
6+
//
7+
8+
import XCTest
9+
import SwiftGodotTestability
10+
@testable import SwiftGodot
11+
12+
@Godot
13+
private class TestProp: Node {
14+
@Export
15+
var changeThisVariable: Int = 1
16+
17+
override func _validateProperty(_ prop: inout PropInfo) -> Bool {
18+
if prop.propertyName == "changeThisVariable" {
19+
prop.usage.insert(.group)
20+
return true
21+
}
22+
return false
23+
}
24+
}
25+
26+
final class TestProperty: GodotTestCase {
27+
override static var godotSubclasses: [Wrapped.Type] {
28+
return [TestProp.self]
29+
}
30+
31+
func testThing() {
32+
var found = false
33+
let node = TestProp()
34+
for prop in node.getPropertyList() {
35+
//print("PROP: \(prop)")
36+
guard let nameV = prop["name"] else { continue }
37+
guard let name = String(nameV) else { continue }
38+
guard let flagsV = prop["usage"], let iflags = Int(flagsV) else { continue }
39+
let flags = PropertyUsageFlags(rawValue: iflags)
40+
41+
if name == "changeThisVariable", flags.contains(.group) {
42+
found = true
43+
}
44+
}
45+
XCTAssertTrue(found, "Should have found a property named hideThisVariable with the usage set to 'readOnly'")
46+
}
47+
}

0 commit comments

Comments
 (0)