Skip to content
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

Open
MichaelBelousov opened this issue Jul 18, 2020 · 4 comments
Open

generate strong types for engine signals and connections #30

MichaelBelousov opened this issue Jul 18, 2020 · 4 comments
Labels
enhancement New feature or request

Comments

@MichaelBelousov
Copy link

MichaelBelousov commented Jul 18, 2020

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)

declare module godot {
  interface SceneTree implements MainLoop {
    //overload 1 for network_peer_connected signal
    function connect<T extends Object>(
      signal: "network_peer_connected",
      target: T,
      method: Extract<T, Function> | (...args: Parameters<onNetworkPeerConnected>) => void,  //could use e.g. MethodOf<T>,
      binds: Tail<Parameters<onNetworkPeerConnected>>,
      flags: number = 0,
    ): number;
    // more overloads, one per signal for discriminating union
  }
}
@MichaelBelousov
Copy link
Author

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;

@Geequlim
Copy link
Owner

In godot signal just a string constant.
That seems a lot of work to do but gains no difference in JavaScript.

@MichaelBelousov
Copy link
Author

MichaelBelousov commented Jul 19, 2020

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.

@Geequlim Geequlim added the enhancement New feature or request label Aug 15, 2020
@jkb0o
Copy link

jkb0o commented May 6, 2021

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
with Godot API, e.g. I am able to emit a signal from the engine using old good emit_signal and so on.

There is Playground Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants