Constructing Arbitrary Types
When interfacing with bevy, we do this via reflection.
While the bevy bindings do not cover constructors for every single type that bevy or other libraries provide, reflection allows us to construct some (not all types implement FromReflect) types from dynamic structs.
BMS exposes this ability to all script writers via the new function available on every type, as well as the more verbose bms.construct.
Type.new
Every type has a new function, taking a table of the fields to set. Types are reached through their namespace, i.e. import_namespace("my_game").MyStruct:
pub struct MyStruct {
pub my_field: String,
pub other_field: usize,
}
local MyStruct = import_namespace("my_game").MyStruct
local my_struct = MyStruct.new({
my_field = "hello",
other_field = 5,
})
pub struct MyTupleStruct(pub String);
Tuple structs and tuples are constructed using the index of the field as the key, the keys follow the indexing convention of the language, i.e. in Lua the first field is "1":
local my_tuple_struct = MyTupleStruct.new({
["1"] = "hello",
})
A list is taken to be the fields in order, which is usually the more natural way to write tuple structs:
local my_tuple_struct = MyTupleStruct.new({ "hello" })
Types which are not structs, tuple structs, tuples or enums, can be constructed only if they implement Default, using Type.new({}).
Defaults
Fields which are not provided are filled in, in the following order of priority:
- the value from the type’s
Defaultimplementation, if it has one. For enums, only if the default is the variant being constructed - the
Defaultof the field’s type, if it has one
If neither is available the construction fails, and the error names the missing field. Entries which are not fields of the type, i.e. a misspelled field name, are an error too, rather than being ignored in favour of a default. The generated Lua language server files mark exactly these fields as optional, see Editor support.
Naming and priority
new is a single function shared by all types, and is made available on every type.
A function with the same name registered on the type itself always takes priority, i.e. Vec3.new(x, y, z) is the Vec3 constructor from the math bindings, not the shared one.
For such types use bms.construct, which works for every type.
The name of the function is configurable via the ConstructorsPlugin:
app.add_plugins(BMSPlugin.set(ConstructorsPlugin {
constructor_name: "create".into(),
}));
The function is registered under the StaticTypeRef type, which is where functions available on all types are registered.
Enums
The following enum:
pub enum MyEnum {
VariantA {
field: String
},
VariantB(usize),
VariantC,
}
has its variants available as MyEnum.VariantA, MyEnum.VariantB and MyEnum.VariantC, which can be used to construct values of the variant using :new:
local a = MyEnum.VariantA:new({ field = "hello" })
local b = MyEnum.VariantB:new({ 5 })
local c = MyEnum.VariantC:new({})
The enum’s Default only fills in fields if it is the variant being constructed.
You can also pick the variant by name using MyEnum.new({ variant = "VariantA", field = "hello" }).
Discriminating variants
The variant a value is in can be obtained with :variant(), and compared with the variants of the enum:
if my_enum:variant() == MyEnum.VariantA then
print(my_enum.field)
end
This is also useful with enums that carry no data, such as keys:
local KeyCode = import_namespace("input").KeyCode
if event.key_code:variant() == KeyCode.KeyA then
print("A was pressed")
end
:variant() returns nil if the value is not an enum. If you need the name of the variant, :variant_name() is also available on both values and variants.
Variants named like functions
Functions on the type take priority over variants, so a variant named like a function cannot be accessed with MyEnum.Name.
Use world.get_enum_variant to get at it in that case:
local world = import_namespace("bms").world
local variant = world.get_enum_variant(import_namespace("my_game").MyEnum, "Name")
bms.construct
The bms.construct function constructs types from a registration.
It works for every type, including those whose own new function shadows the shared one, and is useful if you only have the type as a value:
local bms = import_namespace("bms")
local my_game = import_namespace("my_game") -- the crate defining the types
local concrete_my_struct = bms.construct(my_game.MyStruct, {
my_field = "hello"
})
local variantA = bms.construct(my_game.MyEnum, {
variant = "VariantA",
field = "hello"
})
local vec = bms.construct(import_namespace("math").Vec3, { x = 1, y = 2, z = 3 })
Missing fields are filled in from defaults in the same way as for new.
Editor support
The Lua language server files describe the table each constructor takes, so editors can complete and check the fields. A field is optional exactly when a default can fill it in, as described in Defaults:
#[derive(Reflect)]
pub struct MyStruct {
pub my_field: String,
pub handle: MyHandle, // no `Default`
}
---@class (exact) MyStruct.Fields
---@field my_field? string
---@field handle MyHandle
---@field [any] 'never'
---@param payload MyStruct.Fields
---@return MyStruct
function MyStruct.new(payload) end
The language server then warns about missing required fields, fields of the wrong type, and unknown fields, i.e. a misspelled my_feild.
The last line rejects unknown fields: until the language server checks exact classes against tables passed to functions (LuaLS/lua-language-server#1990), any undeclared field has to be of a type no value has, so the warning reads “Cannot assign string to 'never'”.
Tuple fields are declared under their integer keys, i.e. [1], which a list such as { "hello" } fills in, so prefer lists for tuple structs.
Types and variants without fields all take NoFields, i.e. {}.
Each enum variant has a class of its own, i.e. MyEnum.VariantA, so MyEnum.VariantA:new{ completes the fields of VariantA and returns a MyEnum.
The table of MyEnum.new{ variant = "VariantA", ... } is not described, as the language server cannot narrow its fields down by the variant given.
Prefer MyEnum.VariantA:new{...}, which is checked.
Since the type of bms.construct’s first argument is only known when the script runs, its table is not described.