ribrdb / desynced-tools

Tools for working with behaviors and blueprints from Desynced.
MIT License
4 stars 3 forks source link

Figure out TS syntax for blueprints #38

Closed ribrdb closed 5 months ago

swazrgb commented 6 months ago

What do you think about syntax like this?

// Blueprints must be defined as top level constants
// If exported the string becomes a blueprint string, otherwise the blueprint becomes available to an exported behavior
const example = blueprint("f_building2x1a", {
    // Blueprint name, defaults to variable name. Variable name is used when referencing to this blueprint for instructions.
    name: "My Blueprint",
    // Optional power on/off
    power: true,
    locks: [
        // lock 1st slot to metal ore
        "metalore",
        null,
        // lock 3rd slot to crystal
        "crystal"
    ],
    // Optional logistic settings, defaults to default game settings
    logistics: {
        deliverItems: false,
        channels: [1, 3, 4]
    },
    // We parse visuals.lua & frames.lua to determine the available component slots, so we know how many internal/small/medium/large slots it has and what order they are,
    // so the user doesn't have to worry about this and can just supply an array of components
    // Optional parameters large, medium, small, internal for components
    medium: [
        // keep a slot empty if the user wants for some reason
        null,
        // components can be specified by name if no further configuration is needed
        "c_medium_storage"
    ],
    internal: [
        // components can take configuration, must call the magic component function which provides type hints
        component("c_signal_reader", {
            // optionally set registers
            reg: [
                // first register is linked from a to("reg1"). only a single to statement is allowed per register name.
                from("reg1"),
                // linked to the goto register
                to("goto")
            ]
        }),
        component("c_behavior", {
            // the behavior is referenced as a function
            behavior: func_name,
            reg: [
                // linked to from("reg1") above
                to("reg1"),
                // second parameter set to a static value
                value("metalore", 10)
            ]
        })
    ]
});

function func_name(
    // called reg1 in the blueprint, linked to the signal reader input
    p1: Value,
    // value("metalore", 10)
    p2: Value
) {}

Alternatively, instead of utilizing the magic functions, it could be a pure JSON document. So for example instead of

        component("c_behavior", {
            // the behavior is referenced as a function
            behavior: func_name,
            reg: [
                // linked to from("reg1") above
                to("reg1"),
                // second parameter set to a static value
                value("metalore", 10)
            ]
        })

We could also do

{
  id: "c_behavior",
  behavior: func_name,
  reg: [
    { to: "reg1" },
    { id: "metalore", num: 10 }
  ]
}

This would be a bit more verbose. Both could also be supported at the same time, with the magic functions acting as helpers.

Either way, this would require us to parse the visuals.lua & frames.lua to extract information such as:

data.visuals.v_base2x2b = {
    sockets = {
        { "Medium1", "Medium"  },
        { "Medium2", "Medium"  },
        { "Medium3", "Medium"  },
        { "",       "Internal" },
        { "",       "Internal" },
        { "",       "Internal" },
    },
}

so we can validate/map the components to the correct socket index.

ribrdb commented 6 months ago

I like the syntax using function calls. Maybe we should generate functions for each individual component, instead of having overrides for a "component" function?

swazrgb commented 6 months ago

I'm not sure about generating a function for each component. This would be quite a lot of function names that would become unavailable to users for names as e.g. subroutines. I'd prefer to keep the amount of declarations in the global scope to a minimum.

Though we could also put them in an object... Something like:

internal: [
  component.signalReader([from("reg1"), to("goto")])
]
swazrgb commented 6 months ago

I'm also not sure whether we should always use an object argument, or positional arguments. The big benefit of an object argument is future-proofing, since it allows us to add additional optional or required properties down the line while keeping source backwards-compatability.

But as far as I can tell the game only allows us to configure the registers per component, with the behavior controller being special and also referencing a behavior. I don't think this is likely to change, so we can do:

signalReader(registers: Array<RegisterValue>)

Instead of:

signalReader(args: {
  reg: Array<RegisterValue>
});

We could even go a step further and parse components.lua and extract information about read only registers, and go for:

type ToRegisterLink = { to: string };
type FromRegisterLink = { from: string };
type RegisterLink = ToRegisterLink | FromRegisterLink;
type RegisterValue = RegisterLink | LiteralValue;

signalReader(
  reg?: [
      RegisterValue?,
      // parsed read_only to ToRegisterLink
      ToRegisterLink?
    ]
): InternalComponent;
ribrdb commented 6 months ago

I'm not sure about generating a function for each component. This would be quite a lot of function names that would become unavailable to users for names as e.g. subroutines. I'd prefer to keep the amount of declarations in the global scope to a minimum.

Though we could also put them in an object... Something like:

internal: [
  component.signalReader([from("reg1"), to("goto")])
]

I like that. Mainly I was thinking that with a function we can customize the name to be more user-friendly than the "c_foo" strings.

swazrgb commented 5 months ago

Figured out in https://github.com/ribrdb/desynced-tools/pull/54 :)