-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
generate strong types for engine signals and connections #30
Comments
another place that stronger inference would generate useful tooling: although I would need to research if it's even possible. function rpc_id<T extends {} = {}, MethodKey extends key of T, Method = T[]>(id: number, method: MethodKey, ...args: Parameters<Method>): any; |
In godot signal just a string constant. |
I'm just personally a big fan of strong typing in typescript, and it offers better intellisense/tooling, such as having a type error if you pass arguments that don't correspond to the connected signal. I can look into it and offer a PR and better proof of concept i.e. this.get_tree().connect("network_peer_connected", (id: string, otherArg: number) => {}, "other"); that will be a type error because the id argument should be a number, and a string was passed to the otherArg when a number is required. |
This is an interesting and useful suggestion. I was able to implement such a thing for C# using godot4-like signal declaration. For now, I am on the way to implementing this for our project (and it goes much much simpler). There is a code snippet of how this can work in typescript: // This part should be defined somewhere in the ECMAScript
// The implementation of `emit` and `connect` is omitted to keep things simple
// In real Life each signal instance should keep owenr_id and name (which is set by
// ECMAscript when object instantiating) and bypass connection to singleton proxy Object
// ProxyObject well receive all Godot signals and additional argument bypassed
// by Signal instance - `callback_id` and then calls exact function.
// Godot signal are generated as properties which create instances of Signal
// on demand.
type VarArgs = readonly unknown[];
type Action<T extends VarArgs> = (...args: T) => void;
class Signal<T extends VarArgs> implements PromiseLike<T> {
object_id: Number = 0
name: String = ""
// showcase
callbacks:Action<T>[] = []
public emit(...args: T) {
// something from real world:
// godot.instance_from_id(this.instance_id).emit(this.name, ...args)
// just for showcase
for (const cb of this.callbacks) {
cb(...args)
}
}
public connect(cb: Action<T>) {
// real world:
// let obj = godot.instance_from_id(this.instance_id)
// let callback_id = Proxy.get_instance().register_callback(cb)
// obj.connect(this.name, Proxy.get_instance(), "handle_signal", [callback_id])
// just for showcase
this.callbacks.push(cb);
}
// this is required for awaiting directly for signals
public then<TResult1 = T>(onfulfilled?: ((value: T) => (PromiseLike<TResult1> | TResult1)) | undefined | null): PromiseLike<TResult1> {
return new Promise<TResult1>( (resolve, reject) => {
this.connect((...args: T) => {
if (typeof onfulfilled === 'function') {
resolve(onfulfilled(args))
} else {
// exception?
reject("Don't know how to complete without onfulfilled");
}
})
})
}
}
// the rest part is the usage example
class Obj /* extends godot.Object */ {
// @godot.typedsignal - for registering signal nad providing name and owner_id (or maybe generating property)
pressed: Signal<[]> = new Signal();
input: Signal<[name: String]> = new Signal();
selected: Signal<[name: String, age: Number]> = new Signal();
public async ready() {
console.log("in ready")
this.input.connect(this.handle_input) // <= everything typed as excpected
this.pressed.connect(() => console.log("pressed"))
this.input.connect((name) => console.log("input", name))
this.selected.connect((name, age) => console.log("selected", name, age))
// faking signals for showcase
setTimeout(() => this.pressed.emit(), 100)
setTimeout(() => this.input.emit("jhj"), 200);
setTimeout(() => this.selected.emit("vasia", 21), 300);
console.log("before handle")
await this.handle()
console.log("after handle")
}
public handle_input(name: String) {
console.log("Handling input with handle_input method", name)
}
public async handle(a: String = "" ) {
console.log("in handle")
await this.pressed
console.log("after pressed")
let [key] = await this.input; // key is str
console.log("after input", key)
let [name, age] = await this.selected; // name is str, age is num
console.log("after selected", name, age)
}
}
let n = new Obj()
n.ready()
/* Output:
[LOG]: "in ready"
[LOG]: "before handle"
[LOG]: "in handle"
[LOG]: "pressed"
[LOG]: "after pressed"
[LOG]: "Handling input with handle_input method", "jhj"
[LOG]: "input", "jhj"
[LOG]: "after input", "jhj"
[LOG]: "selected", "vasia", 21
[LOG]: "after selected", "vasia", 21
[LOG]: "after handle"
*/ This approach has some memory overhead but adding a lot of impact and stability. It is also fully compatible There is Playground Link |
is there any interest in generating strong types for connections such as:
(this is probably a poor example, I'd need to dig in and make a PR to do it correctly)
The text was updated successfully, but these errors were encountered: