local utils = require "utils" -- names that would shadow built-in Lua globals are not valid property names -- note that this list is incomplete -- we don't want to include every possible global Northlight function here local invalid_names = utils.make_set{ "assert", "bit32", "collectgarbage", "coroutine", "debug", "error", "io", "ipairs", "math", "next", "os", "pairs", "pcall", "print", "rawget", "rawset", "rawequal", "require", "select", "setmetatable", "string", "table", "tonumber", "tostring", "type", "unpack", "utf8", "xpcall" } local function block_impl(block_name, accessor_name) local _assert = function(cond, msg) if not cond then error(msg, 3) end end local verify_name = function(name) if type(name) ~= "string" then error(tostring(name) .. " is not a valid property name (string expected)", 3) end if not string.match(name, "^[_%a][_%w]*$") then error("'" .. name .. "' is not a valid property name (invalid identifier)", 3) end if invalid_names[name] then error("'" .. name .. "' is not a valid property name (shadowing built-in global)", 3) end end local properties = {} -- get caller's environment -- IMPORTANT: the environment does not exist in WED/NWED so the following code must work with nil env! -- we assume that the game stashes the environment in registry when loading scripts! -- this is dirty but not as dirty as using debug.getinfo magic local env if __get_registry then local environments = __get_registry("environments") local current_script = __get_registry("currently_loading_script") if environments and current_script then env = environments[current_script] end end -- Injects default value to caller's environment (unless already set) local function set_default(property, value) if env and (rawget(env, property.name) == nil or property.has_default_value) then rawset(env, property.name, value) property.has_default_value = true end end local function add_property(property) property.block_name = block_name table.insert(properties, property) if property.default ~= nil then set_default(property, property.default) end end local mt = { int = function(self, name) verify_name(name) add_property{ name = name, type = "int", default = 0 } return self end, float = function(self, name) verify_name(name) add_property{ name = name, type = "float", default = 0.0 } return self end, float2 = function(self, name) verify_name(name) add_property{ name = name, type = "float2", default = vector(0.0, 0.0) } return self end, float3 = function(self, name) verify_name(name) add_property{ name = name, type = "float3", default = vector(0.0, 0.0, 0.0) } return self end, float4 = function(self, name) verify_name(name) add_property{ name = name, type = "float4", default = vector(0.0, 0.0, 0.0, 0.0) } return self end, color = function(self, name) verify_name(name) add_property{ name = name, type = "color", default = vector(0.0, 0.0, 0.0) } return self end, bool = function(self, name) verify_name(name) add_property{ name = name, type = "bool", default = false } return self end, string = function(self, name) verify_name(name) add_property{ name = name, type = "string", default = "" } return self end, entity = function(self, name) verify_name(name) add_property{ name = name, type = "entity_ref" } return self end, entity_type = function(self, component_name) _assert(type(component_name) == "string" and component_name ~= "", "invalid args to 'entity_type' (string expected)") -- DEPRECATED! this never worked return self end, layer = function(self, name) verify_name(name) add_property{ name = name, type = "layer" } return self end, streaming_state = function(self, name) verify_name(name) add_property{ name = name, type = "streaming_state" } return self end, streaming_unit = function(self, name) verify_name(name) add_property{ name = name, type = "streaming_unit" } return self end, asset_node = function(self, name) verify_name(name) add_property{ name = name, type = "asset_node" } return self end, audio_event = function(self, name) verify_name(name) add_property{ name = name, type = "audio_event" } return self end, dialogue_event = function(self, name) verify_name(name) add_property{ name = name, type = "dialogue_event" } return self end, script = function(self, name) verify_name(name) add_property{ name = name, type = "script" } return self end, enum = function(self, name) verify_name(name) add_property{ name = name, type = "enum" } return self end, options = function(self, ...) local opts = table.pack(...) local last_prop = properties[#properties] _assert(opts.n > 0, "invalid use of 'options' - no options given") _assert(last_prop and last_prop.type == "enum", "invalid use of 'options'") _assert(last_prop.default == nil, "enum 'options' must precede 'default'") for i = 1, opts.n do local opt = opts[i] _assert(type(opt) == "string", "invalid use of 'options' - options must be strings") end last_prop.options = opts set_default(last_prop, last_prop.default or opts[1]) return self end, array = function(self) local last_prop = properties[#properties] _assert(last_prop, "invalid use of 'array'") _assert(not last_prop.default_attribute_used, "property cannot be an array and have default value at the same time") _assert(last_prop.type ~= "asset_node", "asset_node properties do not support arrays") _assert(last_prop.type ~= "audio_event", "audio_event properties do not support arrays") _assert(last_prop.type ~= "layer", "layer properties do not support arrays") _assert(last_prop.type ~= "streaming_state", "streaming_state properties do not support arrays") _assert(last_prop.type ~= "streaming_unit", "streaming_unit properties do not support arrays") last_prop.array = true return self end, optional = function(self) local last_prop = properties[#properties] _assert(last_prop, "invalid use of 'optional'") _assert(not last_prop.default_attribute_used, "property cannot be optional and have default value at the same time") last_prop.optional = true -- inject 'false' as property value to caller's environment if property is unset if env and (rawget(env, last_prop.name) == nil or last_prop.has_default_value) then rawset(env, last_prop.name, false) end return self end, default = function(self, value) local last_prop = properties[#properties] _assert(last_prop, "invalid use of 'default'") _assert(last_prop.optional == nil, "property cannot be optional and have default value at the same time") _assert(last_prop.array == nil, "property cannot be an array and have default value at the same time") if last_prop.type == "int" then _assert(type(value) == "number" and math.floor(value) == value, "invalid default for type 'int'") -- we have to check for overflows because converting out of range value from double to int is undefined _assert(value >= -2147483648 and value <= 2147483647, "out of range default value for type 'int'") elseif last_prop.type == "float" then _assert(type(value) == "number", "invalid default for type 'float'") elseif last_prop.type == "float2" or last_prop.type == "float3" or last_prop.type == "float4" or last_prop.type == "color" then _assert(type(value) == "vector", "invalid default for type '" .. last_prop.type .. "'") elseif last_prop.type == "bool" then _assert(type(value) == "boolean", "invalid default for type 'bool'") elseif last_prop.type == "string" then _assert(type(value) == "string", "invalid default for type 'string'") elseif last_prop.type == "enum" then _assert(type(value) == "string", "invalid default for type 'enum'") _assert(last_prop.options == nil or utils.contains(last_prop.options, value), "invalid enum default") else _assert(false, "default value not supported for type '" .. last_prop.type .. "'") end last_prop.default = value last_prop.default_attribute_used = true -- for asserting in optional() set_default(last_prop, value) return self end, description = function(self, text) local last_prop = properties[#properties] _assert(last_prop, "invalid use of 'description'") _assert(type(text) == "string", "invalid description (string expected)'") last_prop.desc = text return self end, range = function(self, min, max) _assert(type(min) == "number" and type(max) == "number", "invalid args to 'range' (two numbers expected)") local last_prop = properties[#properties] _assert(last_prop and last_prop.type == "float", "invalid use of 'range'") last_prop.min = min last_prop.max = max return self end, -- for extracting property blocks, see luatools::executePropertyBlocks [accessor_name] = function() return properties end, } -- If you edit these, update the asset type enum in LuaScriptInfo.cpp as well! local asset_types = { "mesh", "texture", "particle_system", "curve", "archetype", "decal", "behavior_tree", "audio_environment", "material", "audio_event_nwed" } for _, asset_type in ipairs(asset_types) do mt[asset_type] = function(self, name) verify_name(name) add_property{ name = name, type = "asset_ref", asset_type = asset_type } return self end end setmetatable(mt, mt) return mt end -- Inject property_block and data_contract functions to global namespace for easy access function _G.property_block(block_name) if type(block_name) ~= "string" then error("invalid block_name provided for a property_block (string expected)", 2) end return block_impl(block_name, "_get_properties") end function _G.data_contract(contract_name) contract_name = contract_name or "Default" if type(contract_name) ~= "string" then error("invalid contract_name provided for a data_contract (string expected)", 2) end return block_impl(contract_name, "_get_contract_properties") end