From 0e2f4c1f4212677a43c49b4e860df4a26e6c12df Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 1 Jun 2026 10:04:50 -0700 Subject: [PATCH 01/12] WIP fluent list work. --- lsl_definitions.schema.json | 170 +++++++++++++++++++----------------- lsl_definitions.yaml | 27 ++++++ lsl_definitions/lsl.py | 2 + lsl_definitions/rulesets.py | 103 ++++++++++++++++++++-- 4 files changed, 211 insertions(+), 91 deletions(-) diff --git a/lsl_definitions.schema.json b/lsl_definitions.schema.json index 7a50dcfc..8c5aaf65 100644 --- a/lsl_definitions.schema.json +++ b/lsl_definitions.schema.json @@ -149,6 +149,86 @@ "markdownDescription": "An array of parameter type-name pairs.", "minItems": 1, "type": "array" + }, + "builderRule": { + "additionalProperties": false, + "type": "object", + "required": ["params"], + "dependentRequired": { + "variants-on": ["variants", "variant-enum"], + "variants": ["variants-on", "variant-enum"], + "variant-enum": ["variants", "variants-on"] + }, + "properties": { + "face-target": { + "const": true, + "markdownDescription": "This rule targets a specific face.", + "type": "boolean" + }, + "method-name": { + "markdownDescription": "Override the derived SLua builder method name.", + "pattern": "^[a-z][A-Za-z0-9]*$", + "type": "string" + }, + "nullable": { + "const": true, + "markdownDescription": "This rule's params may all be NULL.", + "type": "boolean" + }, + "params": { "$ref": "#/$defs/rulesetParamArray" }, + "variants-on": { + "markdownDescription": "Name of the discriminator parameter (must appear in `params`).", + "pattern": "^[a-z_]+$", + "type": "string" + }, + "variant-enum": { + "markdownDescription": "Name of the LSL enum whose members key into each variant's `applies-to`.", + "pattern": "^[A-Z][A-Za-z0-9]*$", + "type": "string" + }, + "variants": { + "markdownDescription": "Variants whose additional params depend on the discriminator value.", + "items": { + "additionalProperties": false, + "type": "object", + "required": ["applies-to", "params"], + "properties": { + "applies-to": { + "markdownDescription": "Members of the rule's `variant-enum` for which this variant applies.", + "items": { + "pattern": "^[A-Z][A-Z0-9_]*$", + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + }, + "params": { "$ref": "#/$defs/rulesetParamArray" } + } + }, + "minItems": 1, + "type": "array" + } + } + }, + "builderRuleset": { + "additionalProperties": false, + "type": "object", + "required": ["rules"], + "properties": { + "enum": { + "markdownDescription": "Name of the LSL enum whose members are the rule constants. The enum's `prefix` is stripped off each rule name to derive builder method names; every key under `rules` must be a member of this enum.", + "pattern": "^[A-Z][A-Za-z0-9]*$", + "type": "string" + }, + "rules": { + "additionalProperties": false, + "type": "object", + "patternProperties": { + "^[A-Z][A-Z0-9_]*$": { "$ref": "#/$defs/builderRule" } + } + } + } } }, "additionalProperties": false, @@ -221,6 +301,10 @@ "markdownDescription": "The name of the enum this constant is a member of, if any.", "type": "array", "items": { "type": "string", "pattern": "^[A-Z]+[A-Za-z_]*$" } + }, + "value-type": { + "markdownDescription": "For constants used as rule tags in list-based APIs: the type of the value that follows this constant in the list. Uses the same vocabulary as ruleset param types.", + "enum": ["integer", "float", "string", "vector", "rotation", "boolean", "asset", "key"] } }, "required": [ @@ -541,90 +625,12 @@ "type": "object" }, "builder-rulesets": { - "additionalProperties": false, "markdownDescription": "Rulesets that codegen into fluent-builder SLua classes (ordered rule+value setters, face-target, discriminator variants).", "type": "object", - "properties": { - "prim-params": { - "additionalProperties": false, - "type": "object", - "required": ["rules"], - "properties": { - "enum": { - "markdownDescription": "Name of the LSL enum whose members are the rule constants. The enum's `prefix` is stripped off each rule name to derive builder method names; every key under `rules` must be a member of this enum.", - "pattern": "^[A-Z][A-Za-z0-9]*$", - "type": "string" - }, - "rules": { - "additionalProperties": false, - "type": "object", - "patternProperties": { - "^[A-Z][A-Z0-9_]*$": { - "additionalProperties": false, - "type": "object", - "required": ["params"], - "dependentRequired": { - "variants-on": ["variants", "variant-enum"], - "variants": ["variants-on", "variant-enum"], - "variant-enum": ["variants", "variants-on"] - }, - "properties": { - "face-target": { - "const": true, - "markdownDescription": "This rule targets a specific face.", - "type": "boolean" - }, - "method-name": { - "markdownDescription": "Override the derived SLua builder method name.", - "pattern": "^[a-z][A-Za-z0-9]*$", - "type": "string" - }, - "nullable": { - "const": true, - "markdownDescription": "This rule's params may all be NULL.", - "type": "boolean" - }, - "params": { "$ref": "#/$defs/rulesetParamArray" }, - "variants-on": { - "markdownDescription": "Name of the discriminator parameter (must appear in `params`).", - "pattern": "^[a-z_]+$", - "type": "string" - }, - "variant-enum": { - "markdownDescription": "Name of the LSL enum whose members key into each variant's `applies-to`.", - "pattern": "^[A-Z][A-Za-z0-9]*$", - "type": "string" - }, - "variants": { - "markdownDescription": "Variants whose additional params depend on the discriminator value.", - "items": { - "additionalProperties": false, - "type": "object", - "required": ["applies-to", "params"], - "properties": { - "applies-to": { - "markdownDescription": "Members of the rule's `variant-enum` for which this variant applies.", - "items": { - "pattern": "^[A-Z][A-Z0-9_]*$", - "type": "string" - }, - "minItems": 1, - "type": "array", - "uniqueItems": true - }, - "params": { "$ref": "#/$defs/rulesetParamArray" } - } - }, - "minItems": 1, - "type": "array" - } - } - } - } - } - } - } - } + "patternProperties": { + "^[a-z][a-z0-9-]*$": { "$ref": "#/$defs/builderRuleset" } + }, + "additionalProperties": false } }, "required": [ diff --git a/lsl_definitions.yaml b/lsl_definitions.yaml index 512b5744..933388fb 100644 --- a/lsl_definitions.yaml +++ b/lsl_definitions.yaml @@ -4576,11 +4576,13 @@ constants: tooltip: Integer parameter defining the destination blending function. type: integer value: 25 + value-type: integer PSYS_PART_BLEND_FUNC_SOURCE: member-of: [ParticleParam] tooltip: Integer parameter defining the source blending function. type: integer value: 24 + value-type: integer PSYS_PART_BOUNCE_MASK: member-of: [ParticleFlag] tooltip: Particle flag causing particles to bounce off a plane at the emitter @@ -4600,29 +4602,34 @@ constants: particles. type: integer value: 4 + value-type: float PSYS_PART_END_COLOR: member-of: [ParticleParam] tooltip: Vector parameter that determines the ending color of the particles. type: integer value: 3 + value-type: vector PSYS_PART_END_GLOW: member-of: [ParticleParam] tooltip: Float parameter that determines the ending glow value of the particles. type: integer value: 27 + value-type: float PSYS_PART_END_SCALE: member-of: [ParticleParam] tooltip: Vector parameter defining the ending size of the particle billboard in meters (z-axis value is ignored). type: integer value: 6 + value-type: vector PSYS_PART_FLAGS: member-of: [ParticleParam] tooltip: Integer bitfield parameter specifying the simulation flags for emitted particles. Multiple flags can be combined using bitwise OR (|). type: integer value: 0 + value-type: integer PSYS_PART_FOLLOW_SRC_MASK: member-of: [ParticleFlag] tooltip: Particle flag causing particle positions to remain relative to the @@ -4655,6 +4662,7 @@ constants: individual particle before it dies. type: integer value: 7 + value-type: float PSYS_PART_RIBBON_MASK: member-of: [ParticleFlag] tooltip: Particle flag that joins emitted particles into a continuous ribbon @@ -4670,23 +4678,27 @@ constants: the particles. type: integer value: 2 + value-type: float PSYS_PART_START_COLOR: member-of: [ParticleParam] tooltip: Vector parameter that determines the starting color of the particles. type: integer value: 1 + value-type: vector PSYS_PART_START_GLOW: member-of: [ParticleParam] tooltip: Float parameter that determines the starting glow value of the particles. type: integer value: 26 + value-type: float PSYS_PART_START_SCALE: member-of: [ParticleParam] tooltip: Vector parameter defining the starting size of the particle billboard in meters (z-axis value is ignored). type: integer value: 5 + value-type: vector PSYS_PART_TARGET_LINEAR_MASK: member-of: [ParticleFlag] tooltip: Particle flag causing emitted particles to move in a straight, @@ -4716,12 +4728,14 @@ constants: apply to particles. type: integer value: 8 + value-type: vector PSYS_SRC_ANGLE_BEGIN: member-of: [ParticleParam] tooltip: Float parameter specifying the angle in radians where particles will not be created (for ANGLE patterns). type: integer value: 22 + value-type: float PSYS_SRC_ANGLE_END: member-of: [ParticleParam] tooltip: Float parameter specifying the ending angle in radians to be filled @@ -4729,45 +4743,53 @@ constants: roles are swapped. type: integer value: 23 + value-type: float PSYS_SRC_BURST_PART_COUNT: member-of: [ParticleParam] tooltip: Integer parameter specifying the number of particles to release in each burst. type: integer value: 15 + value-type: integer PSYS_SRC_BURST_RADIUS: member-of: [ParticleParam] tooltip: Float parameter specifying the radial distance from the emitter's center at which particles are created. type: integer value: 16 + value-type: float PSYS_SRC_BURST_RATE: member-of: [ParticleParam] tooltip: Float parameter specifying the interval in seconds between particle bursts. type: integer value: 13 + value-type: float PSYS_SRC_BURST_SPEED_MAX: member-of: [ParticleParam] tooltip: Float parameter specifying the maximum initial speed of emitted particles. type: integer value: 18 + value-type: float PSYS_SRC_BURST_SPEED_MIN: member-of: [ParticleParam] tooltip: Float parameter specifying the minimum initial speed of emitted particles. type: integer value: 17 + value-type: float PSYS_SRC_INNERANGLE: member-of: [ParticleParam] tooltip: Float parameter specifying the inner angle of the arc for ANGLE or ANGLE_CONE patterns. The specified inner area will not contain particles. type: integer value: 10 + value-type: float PSYS_SRC_MAX_AGE: member-of: [ParticleParam] tooltip: Float parameter specifying the active duration of the particle system in seconds (0.0 means forever). type: integer value: 19 + value-type: float # These were present in the lexer, but not in the syntax file, they're marked private for consistency. # TODO: check if the private is actually warranted, it's likely not. PSYS_SRC_OBJ_REL_MASK: @@ -4782,6 +4804,7 @@ constants: emission axis for ANGLE and ANGLE_CONE patterns. type: integer value: 21 + value-type: vector PSYS_SRC_OUTERANGLE: member-of: [ParticleParam] tooltip: Float parameter specifying the outer angle of the arc for ANGLE or @@ -4789,12 +4812,14 @@ constants: with particles. type: integer value: 11 + value-type: float PSYS_SRC_PATTERN: member-of: [ParticleParam] tooltip: Integer parameter specifying the generation pattern (PSYS_SRC_PATTERN_*) used to emit particles. type: integer value: 9 + value-type: integer PSYS_SRC_PATTERN_ANGLE: member-of: [ParticleSourcePattern] tooltip: Emission pattern that sprays particles outward in a flat circular, @@ -4836,12 +4861,14 @@ constants: PSYS_PART_TARGET_POS_MASK or PSYS_PART_TARGET_LINEAR_MASK is enabled. type: integer value: 20 + value-type: key PSYS_SRC_TEXTURE: member-of: [ParticleParam] tooltip: String parameter specifying the inventory name or UUID asset key of the texture used for the particles. type: integer value: 12 + value-type: asset PUBLIC_CHANNEL: tooltip: Integer constant representing the open local chat channel. Passing this to chat functions (llSay, llWhisper, llShout) prints text to the publicly diff --git a/lsl_definitions/lsl.py b/lsl_definitions/lsl.py index 1bc709d1..64aa8e86 100644 --- a/lsl_definitions/lsl.py +++ b/lsl_definitions/lsl.py @@ -153,6 +153,7 @@ class LSLConstant: private: bool """Whether this should this be included in the syntax file""" member_of: list[LSLEnum] = dataclasses.field(default_factory=list) + value_type: Optional[str] = None @property def value_raw(self) -> str: @@ -782,6 +783,7 @@ def _handle_constant(self, const_name: str, const_data: dict) -> LSLConstant: slua_deprecated=Deprecated.from_definition( const_data.get("slua-deprecated", False) ), + value_type=const_data.get("value-type", None), ) if const.type not in {"float", "integer", "string", "vector", "rotation"}: raise ValueError(f"Invalid constant type {const.type}") diff --git a/lsl_definitions/rulesets.py b/lsl_definitions/rulesets.py index af043a77..329f7d5b 100644 --- a/lsl_definitions/rulesets.py +++ b/lsl_definitions/rulesets.py @@ -1,12 +1,13 @@ """ -Ruleset-to-Luau codegen. Currently the only ruleset modeled is `prim-params`, -expanded into the fluent SPP builder. +Ruleset-to-Luau codegen. Supports any named ruleset declared under +builder-rulesets in lsl_definitions.yaml. The fluent SPP builder for +`prim-params` is the reference implementation. """ from __future__ import annotations import dataclasses -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Set if TYPE_CHECKING: from lsl_definitions.lsl import LSLDefinitions @@ -63,6 +64,20 @@ class BuilderSpec: methods: list[BuilderMethod] +@dataclasses.dataclass(frozen=True) +class MemberDescriptor: + """Descriptor for a single property in an enum-membership-derived builder.""" + + strict_name: str + """Always-valid property name (prefix stripped, lowercased).""" + pretty_name: Optional[str] + """Pretty alias, or None if reverted due to collision.""" + tag: int + """Numeric value of the originating constant (the rule tag).""" + value_type: str + """Semantic type from the value-type annotation (e.g. 'float', 'vector').""" + + def _method_name(*parts: str) -> str: # ("COLOR",) -> "color", ("GLTF_BASE_COLOR",) -> "gltfBaseColor"; # ("PRIM_", "TYPE", "BOX") -> "primTypeBox". @@ -78,11 +93,13 @@ def _build_params(raw_params: list[tuple[str, str]]) -> list[BuilderMethodParam] ] -def expand_spp_builder(lsl: LSLDefinitions) -> BuilderSpec: - # Translates the `prim-params` ruleset into SLua builder methods. The - # LSL-side semantic validation already ran in - # `LSLDefinitionParser._validate_builder_rule_variants`. - ruleset = lsl.builder_rulesets["prim-params"] +def expand_builder(lsl: LSLDefinitions, ruleset_name: str, class_name: str) -> BuilderSpec: + """Expand any named builder-ruleset into a BuilderSpec. + + LSL-side semantic validation is assumed to have already run via + LSLDefinitionParser._validate_builder_rule_variants. + """ + ruleset = lsl.builder_rulesets[ruleset_name] rule_enum = lsl.enums[ruleset["enum"]] methods: list[BuilderMethod] = [] @@ -120,6 +137,74 @@ def expand_spp_builder(lsl: LSLDefinitions) -> BuilderSpec: ) return BuilderSpec( # The "class name" is global, so we don't want it to be ambiguous - class_name="PrimParamsSetterType", + class_name=class_name, methods=methods, ) + + +def expand_spp_builder(lsl: LSLDefinitions) -> BuilderSpec: + """Compatibility wrapper. Expands the prim-params ruleset into the SPP builder.""" + return expand_builder(lsl, "prim-params", "PrimParamsSetterType") + + +def expand_member_params( + lsl: LSLDefinitions, + enum_name: str, + filler_tokens: Set[str], +) -> List[MemberDescriptor]: + """Derive a sorted, collision-resolved list of property descriptors from + the constants that are members of *enum_name*. + + Pass 1 produces strict names by stripping the enum prefix and lowercasing. + Pass 2 produces pretty aliases by removing *filler_tokens*. + If two pretty aliases collide, both revert to their strict names. + Fails hard if any two strict names are identical. + """ + enum = lsl.enums[enum_name] + candidates = sorted( + ( + c + for c in lsl.constants.values() + if any(e.name == enum_name for e in c.member_of) and not c.private + ), + key=lambda c: int(c.value, 0), + ) + + rows: List[tuple] = [] + for const in candidates: + strict = const.name.removeprefix(enum.prefix).lower() + if const.value_type is None: + raise ValueError( + f"Constant {const.name!r} is a member of {enum_name!r} " + f"but has no value-type annotation." + ) + rows.append((strict, int(const.value, 0), const.value_type)) + + # Hard-fail on strict name collisions (implies a data error). + strict_names = [r[0] for r in rows] + dupes = {n for n in strict_names if strict_names.count(n) > 1} + if dupes: + raise ValueError(f"Strict name collision(s) in {enum_name}: {sorted(dupes)}") + + # Pass 2: pretty aliases. + def _pretty(strict: str) -> str: + tokens = [t for t in strict.split("_") if t not in filler_tokens] + return "_".join(tokens) if tokens else strict + + pretties = [_pretty(strict) for strict, _, _ in rows] + colliding = {p for p in pretties if pretties.count(p) > 1} + + return [ + MemberDescriptor( + strict_name=strict, + pretty_name=None if pretty in colliding else pretty, + tag=tag, + value_type=vt, + ) + for (strict, tag, vt), pretty in zip(rows, pretties) + ] + + +def expand_particle_params(lsl: LSLDefinitions) -> List[MemberDescriptor]: + """Expand ParticleParam members with the standard particle filler tokens.""" + return expand_member_params(lsl, "ParticleParam", {"part", "src"}) From 7e4dad2d085125b524201fd0301dc63d24671c82 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Thu, 4 Jun 2026 16:14:51 +0000 Subject: [PATCH 02/12] Add rulesets for generating table based way of passing parameters to functions requiring lists. --- .gitignore | 1 + gen_all_definitions.sh | 1 + generated/cpp/fluent_builder_descriptors.cpp | 47 +++++++ generated/lua_keywords_pretty.xml | 5 + generated/secondlife.d.luau | 47 +++++++ generated/secondlife_selene.yml | 131 +++++++++++++++++++ lsl_definitions.schema.json | 62 ++++++++- lsl_definitions.yaml | 13 ++ lsl_definitions/generators/lscript.py | 106 +++++++++++++++ lsl_definitions/lsl.py | 17 +-- lsl_definitions/rulesets.py | 16 ++- lsl_definitions/slua.py | 78 ++++++++++- slua_definitions.yaml | 21 +++ 13 files changed, 527 insertions(+), 18 deletions(-) create mode 100644 generated/cpp/fluent_builder_descriptors.cpp diff --git a/.gitignore b/.gitignore index 50b9bf70..0f5f4c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ stage/ # Editors .vscode +*.code-workspace diff --git a/gen_all_definitions.sh b/gen_all_definitions.sh index 734fac85..424db987 100755 --- a/gen_all_definitions.sh +++ b/gen_all_definitions.sh @@ -33,6 +33,7 @@ $CMD $DEFS gen_lscript_interface "$outdir/cpp/lscript_interface.cpp" $CMD $DEFS gen_mono_bind_interfaces "$outdir/cpp/mono_bind_interfaces.cpp" $CMD $DEFS gen_lua_registrations 1 "$outdir/cpp/lua_registrations.cpp" $CMD $DEFS gen_lua_constant_definitions "$outdir/cpp/lua_constant_definitions.cpp" +$CMD $DEFS gen_fluent_builder_descriptors "$outdir/cpp/fluent_builder_descriptors.cpp" $CMD $DEFS gen_lscript_library_bind_pure "$outdir/cpp/lscript_library_bind_pure.cpp" $CMD $DEFS gen_tree_header_file "$outdir/cpp/lscript_tree.h" $CMD $DEFS gen_tree_source_file "$outdir/cpp/lscript_tree.cpp" diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp new file mode 100644 index 00000000..681ea513 --- /dev/null +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -0,0 +1,47 @@ +// particle-params +static const FluentParamDescriptor kParticleParamsDescs[] = { + {"flags", 'i', 0}, + {"start_color", 'v', 1}, + {"start_alpha", 'f', 2}, + {"end_color", 'v', 3}, + {"end_alpha", 'f', 4}, + {"start_scale", 'v', 5}, + {"end_scale", 'v', 6}, + {"part_max_age", 'f', 7}, + {"accel", 'v', 8}, + {"pattern", 'i', 9}, + {"innerangle", 'f', 10}, + {"outerangle", 'f', 11}, + {"texture", 'a', 12}, + {"burst_rate", 'f', 13}, + {"burst_count", 'i', 15}, + {"burst_radius", 'f', 16}, + {"burst_speed_min", 'f', 17}, + {"burst_speed_max", 'f', 18}, + {"src_max_age", 'f', 19}, + {"target_key", 'k', 20}, + {"omega", 'v', 21}, + {"angle_begin", 'f', 22}, + {"angle_end", 'f', 23}, + {"blend_func_source", 'i', 24}, + {"blend_func_dest", 'i', 25}, + {"start_glow", 'f', 26}, + {"end_glow", 'f', 27}, +}; +static FluentBuilderDef* kParticleParamsDef = fluent_builder_def_build( + "ParticleSystem", "LinkParticleSystem", + kParticleParamsDescs, std::size(kParticleParamsDescs)); +slua_open_fluent_builder(L, "llparticle", "ParticleParams", kParticleParamsDef); +static const FluentFlagDescriptor kParticleParamFlagDescs[] = { + {"interp_color", 0x1, 0}, + {"interp_scale", 0x2, 0}, + {"bounce", 0x4, 0}, + {"wind", 0x8, 0}, + {"follow", 0x10, 0}, + {"follow_velocity", 0x20, 0}, + {"target_pos", 0x40, 0}, + {"target_linear", 0x80, 0}, + {"emissive", 0x100, 0}, + {"ribbon", 0x400, 0}, +}; +fluent_builder_def_add_flags(kParticleParamsDef, kParticleParamFlagDescs, std::size(kParticleParamFlagDescs)); \ No newline at end of file diff --git a/generated/lua_keywords_pretty.xml b/generated/lua_keywords_pretty.xml index 2a53fe0d..ccaa5255 100644 --- a/generated/lua_keywords_pretty.xml +++ b/generated/lua_keywords_pretty.xml @@ -219,6 +219,11 @@ export type rotation = quaternion tooltip Timer management class for scheduling periodic and one-time callbacks + ParticleParams + + tooltip + Particle system parameter table. Set properties and call apply() to emit particles. + PrimParamsSetterType tooltip diff --git a/generated/secondlife.d.luau b/generated/secondlife.d.luau index 16223968..1b65c167 100644 --- a/generated/secondlife.d.luau +++ b/generated/secondlife.d.luau @@ -169,6 +169,53 @@ declare extern type LLTimers with function off(self, callback: LLTimerCallback): boolean end +export type ParticleParamsMeta = { + __index: ParticleParamsMeta, + flags: number?, + start_color: vector?, + start_alpha: number?, + end_color: vector?, + end_alpha: number?, + start_scale: vector?, + end_scale: vector?, + part_max_age: number?, + accel: vector?, + pattern: number?, + innerangle: number?, + outerangle: number?, + texture: (string | uuid)?, + burst_rate: number?, + burst_count: number?, + burst_radius: number?, + burst_speed_min: number?, + burst_speed_max: number?, + src_max_age: number?, + target_key: (string | uuid)?, + omega: vector?, + angle_begin: number?, + angle_end: number?, + blend_func_source: number?, + blend_func_dest: number?, + start_glow: number?, + end_glow: number?, + interp_color: boolean?, + interp_scale: boolean?, + bounce: boolean?, + wind: boolean?, + follow: boolean?, + follow_velocity: boolean?, + target_pos: boolean?, + target_linear: boolean?, + emissive: boolean?, + ribbon: boolean?, + new: (init: {[string]: any}?) -> ParticleParams, + apply: (self: ParticleParams, linkNumber: number?) -> (), +} + +export type ParticleParams = typeof( + setmetatable((nil :: any) :: {}, (nil :: any) :: ParticleParamsMeta) +) + export type PrimParamsSetterTypeMeta = { __index: PrimParamsSetterTypeMeta, new: () -> PrimParamsSetterType, diff --git a/generated/secondlife_selene.yml b/generated/secondlife_selene.yml index 3ab0c5f6..8d4a17bb 100644 --- a/generated/secondlife_selene.yml +++ b/generated/secondlife_selene.yml @@ -16106,6 +16106,137 @@ structs: args: - type: function description: Unregisters a timer callback. Returns true if found and removed. + ParticleParams: + apply: + method: true + args: + - type: number + required: false + description: Emit the particle system. Omit linkNumber to call ll.ParticleSystem, + or pass a link number for ll.LinkParticleSystem. + new: + args: + - type: table + required: false + description: Create a new ParticleParams, optionally bulk-initializing from + a table. + flags: + property: full-write + type: number + start_color: + property: full-write + type: + display: vector + start_alpha: + property: full-write + type: number + end_color: + property: full-write + type: + display: vector + end_alpha: + property: full-write + type: number + start_scale: + property: full-write + type: + display: vector + end_scale: + property: full-write + type: + display: vector + part_max_age: + property: full-write + type: number + accel: + property: full-write + type: + display: vector + pattern: + property: full-write + type: number + innerangle: + property: full-write + type: number + outerangle: + property: full-write + type: number + texture: + property: full-write + type: string + burst_rate: + property: full-write + type: number + burst_count: + property: full-write + type: number + burst_radius: + property: full-write + type: number + burst_speed_min: + property: full-write + type: number + burst_speed_max: + property: full-write + type: number + src_max_age: + property: full-write + type: number + target_key: + property: full-write + type: string + omega: + property: full-write + type: + display: vector + angle_begin: + property: full-write + type: number + angle_end: + property: full-write + type: number + blend_func_source: + property: full-write + type: number + blend_func_dest: + property: full-write + type: number + start_glow: + property: full-write + type: number + end_glow: + property: full-write + type: number + interp_color: + property: full-write + type: bool + interp_scale: + property: full-write + type: bool + bounce: + property: full-write + type: bool + wind: + property: full-write + type: bool + follow: + property: full-write + type: bool + follow_velocity: + property: full-write + type: bool + target_pos: + property: full-write + type: bool + target_linear: + property: full-write + type: bool + emissive: + property: full-write + type: bool + ribbon: + property: full-write + type: bool PrimParamsSetterType: apply: method: true diff --git a/lsl_definitions.schema.json b/lsl_definitions.schema.json index 8c5aaf65..8966c65e 100644 --- a/lsl_definitions.schema.json +++ b/lsl_definitions.schema.json @@ -214,21 +214,73 @@ "builderRuleset": { "additionalProperties": false, "type": "object", - "required": ["rules"], + "required": ["type", "enum"], "properties": { + "type": { + "markdownDescription": "The kind of fluent interface this ruleset generates. `builder` produces setter methods (SPP-style); `table` produces property-assignment tables with an `apply()` dispatch.", + "type": "string", + "enum": ["builder", "table"] + }, "enum": { - "markdownDescription": "Name of the LSL enum whose members are the rule constants. The enum's `prefix` is stripped off each rule name to derive builder method names; every key under `rules` must be a member of this enum.", - "pattern": "^[A-Z][A-Za-z0-9]*$", - "type": "string" + "markdownDescription": "Name of the LSL enum backing this ruleset. For `builder` rulesets, the enum's `prefix` is stripped from rule constant names to derive method names, and every key under `rules` must be a member of this enum. For `table` rulesets, members of this enum map directly to table properties.", + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9]*$" }, "rules": { + "markdownDescription": "(`builder` only) Map of rule constant names to their rule definitions.", "additionalProperties": false, "type": "object", "patternProperties": { "^[A-Z][A-Z0-9_]*$": { "$ref": "#/$defs/builderRule" } } + }, + "filler-tokens": { + "markdownDescription": "(`table` optional) Tokens stripped from member names when generating pretty property aliases.", + "type": "array", + "items": { "type": "string", "pattern": "^[a-z]+$" } + }, + "flag-enum": { + "markdownDescription": "(`table` optional) Name of the LSL flag enum whose bits are exposed as boolean properties.", + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9]*$" + }, + "flag-field": { + "markdownDescription": "(`table` optional) Name of the integer property that `flag-enum` bits are ORed into.", + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "flag-mask": { + "markdownDescription": "(`table` optional) Suffix (e.g. `_MASK`) stripped from each flag constant name before tokenizing into a property name.", + "type": "string", + "pattern": "^_[A-Z]+$" + }, + "lua-module": { + "markdownDescription": "(`table` only) Lua global module name (e.g. `llparticle`).", + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "lua-type": { + "markdownDescription": "(`table` only) Lua type name within the module (e.g. `ParticleParams`).", + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9]*$" + }, + "apply-fn": { + "markdownDescription": "(`table` only) `ll.*` function called by `apply()` with no link number (e.g. `ParticleSystem`).", + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9]*$" + }, + "apply-link-fn": { + "markdownDescription": "(`table` only) `ll.*` function called by `apply(linkNumber)` (e.g. `LinkParticleSystem`).", + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9]*$" } - } + }, + "if": { + "properties": { "type": { "const": "builder" } }, + "required": ["type"] + }, + "then": { "required": ["rules"] }, + "else": { "required": ["lua-module", "lua-type", "apply-fn", "apply-link-fn"] } } }, "additionalProperties": false, diff --git a/lsl_definitions.yaml b/lsl_definitions.yaml index 933388fb..d145d314 100644 --- a/lsl_definitions.yaml +++ b/lsl_definitions.yaml @@ -16705,6 +16705,7 @@ controls: # details, media, environment, KFM options, prim-param reads) aren't modeled yet. builder-rulesets: prim-params: + type: builder enum: PrimParam rules: # PRIM_TYPE_LEGACY intentionally omitted, it has very annoying semantics, @@ -16970,3 +16971,15 @@ builder-rulesets: params: - [ asset, sound ] - [ float, volume ] + + particle-params: + type: table + enum: ParticleParam + filler-tokens: [part, src] + flag-enum: ParticleFlag + flag-field: flags + flag-mask: _MASK + lua-module: llparticle + lua-type: ParticleParams + apply-fn: ParticleSystem + apply-link-fn: LinkParticleSystem diff --git a/lsl_definitions/generators/lscript.py b/lsl_definitions/generators/lscript.py index 607adc31..a0cff08c 100644 --- a/lsl_definitions/generators/lscript.py +++ b/lsl_definitions/generators/lscript.py @@ -7,6 +7,7 @@ from lsl_definitions.generators.base import register from lsl_definitions.lsl import LSLDefinitions, LSLFunction, LSLType +from lsl_definitions.rulesets import expand_table_ruleset from lsl_definitions.utils import is_uuid, splice_str, to_c_str @@ -883,6 +884,111 @@ def gen_lua_constant_definitions(definitions: LSLDefinitions) -> str: return "\n".join(bindings) +@register("gen_fluent_builder_descriptors") +def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: + """Generate FluentParamDescriptor arrays and init calls for all type:table builder-rulesets. + + Reads each ruleset's metadata (enum, filler-tokens, apply-fn, apply-link-fn, + lua-module, lua-type) from lsl_definitions.yaml. The resulting C++ fragment is + meant to be #include'd directly inside init_fluent_builders(lua_State* L). + """ + _semantic_map = { + "integer": "i", + "float": "f", + "string": "s", + "vector": "v", + "rotation": "r", + "boolean": "b", + "asset": "a", + "key": "k", + } + + sections = [] + for ruleset_name, ruleset_data in definitions.builder_rulesets.items(): + if ruleset_data.get("type", "builder") != "table": + continue + + enum_name = ruleset_data["enum"] + lua_module = ruleset_data["lua-module"] + lua_type = ruleset_data["lua-type"] + apply_fn = ruleset_data["apply-fn"] + apply_link_fn = ruleset_data["apply-link-fn"] + + # Descriptor array/def names derived from the enum name. + # e.g. ParticleParam -> kParticleParamsDescs, kParticleParamsDef + array_name = f"k{enum_name}sDescs" + def_name = f"k{enum_name}sDef" + + desc_lines = [] + for desc in expand_table_ruleset(definitions, ruleset_name): + name = desc.pretty_name if desc.pretty_name else desc.strict_name + sem = _semantic_map[desc.value_type] + desc_lines.append(f" {{\"{name}\", '{sem}', {desc.tag}}},") + + body = "\n".join(desc_lines) + section = ( + f"// {ruleset_name}\n" + f"static const FluentParamDescriptor {array_name}[] = {{\n{body}\n}};\n" + f"static FluentBuilderDef* {def_name} = fluent_builder_def_build(\n" + f' "{apply_fn}", "{apply_link_fn}",\n' + f" {array_name}, std::size({array_name}));\n" + f'slua_open_fluent_builder(L, "{lua_module}", "{lua_type}", {def_name});' + ) + + flag_enum_name = ruleset_data.get("flag-enum") + flag_field = ruleset_data.get("flag-field") + if flag_enum_name and flag_field: + flag_enum = definitions.enums[flag_enum_name] + filler_tokens = set(ruleset_data.get("filler-tokens", [])) + prefix = flag_enum.prefix + + # Find field_tag from the already-computed param descriptors. + field_tag = None + for desc in expand_table_ruleset(definitions, ruleset_name): + eff_name = desc.pretty_name if desc.pretty_name else desc.strict_name + if eff_name == flag_field: + field_tag = desc.tag + break + if field_tag is None: + raise ValueError( + f"{ruleset_name}: flag-field '{flag_field}' not found in descriptor list" + ) + + # Enumerate flag constants sorted by numeric value. + flag_consts = sorted( + ( + c + for c in definitions.constants.values() + if any(e.name == flag_enum_name for e in c.member_of) and not c.private + ), + key=lambda c: int(c.value, 0), + ) + + flag_suffix = (ruleset_data.get("flag-mask") or "").lower() + + flag_array_name = f"k{enum_name}FlagDescs" + flag_lines = [] + for const in flag_consts: + name = const.name + strict = (name[len(prefix) :] if name.startswith(prefix) else name).lower() + if flag_suffix and strict.endswith(flag_suffix): + strict = strict[: -len(flag_suffix)] + tokens = [t for t in strict.split("_") if t not in filler_tokens] + prop_name = "_".join(tokens) if tokens else strict + mask = int(const.value, 0) + flag_lines.append(f' {{"{prop_name}", 0x{mask:x}, {field_tag}}},') + + flag_body = "\n".join(flag_lines) + section += ( + f"\nstatic const FluentFlagDescriptor {flag_array_name}[] = {{\n{flag_body}\n}};\n" + f"fluent_builder_def_add_flags({def_name}, {flag_array_name}, std::size({flag_array_name}));" + ) + + sections.append(section) + + return "\n\n".join(sections) + + @register("gen_lscript_library_bind_pure") def gen_lscript_library_bind_pure(definitions: LSLDefinitions) -> str: """Generate pure function bindings for test harnesses diff --git a/lsl_definitions/lsl.py b/lsl_definitions/lsl.py index 64aa8e86..58519bb6 100644 --- a/lsl_definitions/lsl.py +++ b/lsl_definitions/lsl.py @@ -571,14 +571,15 @@ def _parse_dict(self, def_dict: dict) -> LSLDefinitions: enum_name = ruleset_data["enum"] if enum_name not in self._definitions.enums: raise ValueError(f"{ruleset_name} references unknown enum {enum_name!r}") - rule_enum = self._definitions.enums[enum_name] - for rule_name, rule_data in ruleset_data["rules"].items(): - if rule_name not in rule_enum.by_name: - raise ValueError( - f"{ruleset_name} rule {rule_name!r} is not a member of " - f"its ruleset's declared enum {enum_name!r}" - ) - self._validate_builder_rule_variants(ruleset_name, rule_name, rule_data) + if ruleset_data.get("type", "builder") == "builder": + rule_enum = self._definitions.enums[enum_name] + for rule_name, rule_data in ruleset_data["rules"].items(): + if rule_name not in rule_enum.by_name: + raise ValueError( + f"{ruleset_name} rule {rule_name!r} is not a member of " + f"its ruleset's declared enum {enum_name!r}" + ) + self._validate_builder_rule_variants(ruleset_name, rule_name, rule_data) self._definitions.builder_rulesets.update(builder_rulesets) return self._definitions diff --git a/lsl_definitions/rulesets.py b/lsl_definitions/rulesets.py index 329f7d5b..2096e0d2 100644 --- a/lsl_definitions/rulesets.py +++ b/lsl_definitions/rulesets.py @@ -172,7 +172,9 @@ def expand_member_params( rows: List[tuple] = [] for const in candidates: - strict = const.name.removeprefix(enum.prefix).lower() + name = const.name + prefix = enum.prefix + strict = (name[len(prefix) :] if name.startswith(prefix) else name).lower() if const.value_type is None: raise ValueError( f"Constant {const.name!r} is a member of {enum_name!r} " @@ -205,6 +207,12 @@ def _pretty(strict: str) -> str: ] -def expand_particle_params(lsl: LSLDefinitions) -> List[MemberDescriptor]: - """Expand ParticleParam members with the standard particle filler tokens.""" - return expand_member_params(lsl, "ParticleParam", {"part", "src"}) +def expand_table_ruleset(lsl: LSLDefinitions, ruleset_name: str) -> List[MemberDescriptor]: + """Expand a type:table builder-ruleset into a sorted property descriptor list. + + Reads ``enum`` and ``filler-tokens`` from the yaml entry. + """ + ruleset = lsl.builder_rulesets[ruleset_name] + enum_name = ruleset["enum"] + filler_tokens = set(ruleset.get("filler-tokens", [])) + return expand_member_params(lsl, enum_name, filler_tokens) diff --git a/lsl_definitions/slua.py b/lsl_definitions/slua.py index fa27a18a..8639550c 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -10,9 +10,23 @@ import llsd import yaml -from lsl_definitions.rulesets import BuilderMethod, BuilderSpec, expand_spp_builder +from lsl_definitions.rulesets import ( + BuilderMethod, + BuilderSpec, + expand_spp_builder, + expand_table_ruleset, +) from lsl_definitions.utils import Deprecated, remove_worthless +_TABLE_RULESET_TYPE_MAP: dict[str, str] = { + "float": "number", + "integer": "number", + "vector": "vector", + "string": "string", + "key": "(string | uuid)", + "asset": "(string | uuid)", +} + if TYPE_CHECKING: from typing import Literal @@ -418,6 +432,7 @@ def finalize(self, lsl: LSLDefinitions) -> None: """ self.generate_ll_modules(lsl) self._generate_spp_builder_class(lsl) + self._generate_fluent_builder_classes(lsl) def generate_ll_modules(self, lsl: LSLDefinitions, solverV2: bool = True) -> None: """ @@ -652,6 +667,67 @@ def make_fluent_method(builder_spec: BuilderSpec, method: BuilderMethod) -> SLua builder_class = [m for m in self.classes if m.name == spec.class_name][0] builder_class.methods.extend(methods) + def _generate_fluent_builder_classes(self, lsl: "LSLDefinitions") -> None: + """Inject typed properties into fluent builder classes declared in slua_definitions.yaml. + + For each table-type builder ruleset that names a lua-type, finds the matching + SLuaClassDeclaration in self.classes and populates its properties list from + expand_table_ruleset() plus any flag enum members. + """ + for ruleset_name, ruleset_data in lsl.builder_rulesets.items(): + if ruleset_data.get("type") != "table": + continue + lua_type = ruleset_data.get("lua-type") + if not lua_type: + continue + try: + cls = next(c for c in self.classes if c.name == lua_type) + except StopIteration: + continue + + # Regular properties from expand_table_ruleset + for desc in expand_table_ruleset(lsl, ruleset_name): + prop_name = desc.pretty_name if desc.pretty_name else desc.strict_name + luau_base = _TABLE_RULESET_TYPE_MAP.get(desc.value_type, "any") + cls.properties.append( + SLuaProperty( + name=prop_name, + type=f"{luau_base}?", + modifiable="full-write", + ) + ) + + # Flag enum boolean properties + flag_enum_name = ruleset_data.get("flag-enum") + if flag_enum_name: + flag_enum = lsl.enums[flag_enum_name] + prefix = flag_enum.prefix + flag_suffix = (ruleset_data.get("flag-mask") or "").lower() + filler_tokens = set(ruleset_data.get("filler-tokens", [])) + flag_consts = sorted( + ( + c + for c in lsl.constants.values() + if any(e.name == flag_enum_name for e in c.member_of) and not c.private + ), + key=lambda c: int(c.value, 0), + ) + for const in flag_consts: + strict = ( + const.name[len(prefix) :] if const.name.startswith(prefix) else const.name + ).lower() + if flag_suffix and strict.endswith(flag_suffix): + strict = strict[: -len(flag_suffix)] + tokens = [t for t in strict.split("_") if t not in filler_tokens] + prop_name = "_".join(tokens) if tokens else strict + cls.properties.append( + SLuaProperty( + name=prop_name, + type="boolean?", + modifiable="full-write", + ) + ) + class SLuaDefinitionParser: def __init__(self): diff --git a/slua_definitions.yaml b/slua_definitions.yaml index cb5141b5..64d6522b 100644 --- a/slua_definitions.yaml +++ b/slua_definitions.yaml @@ -317,6 +317,27 @@ classes: - name: callback type: LLTimerCallback return-type: boolean +- name: ParticleParams + comment: Particle system parameter table. Set properties and call apply() to emit particles. + instance-type: '{}' + export: true + properties: [] # injected at generation time from expand_table_ruleset("particle-params") + functions: + - name: new + comment: Create a new ParticleParams, optionally bulk-initializing from a table. + parameters: + - name: init + type: '{[string]: any}?' + return-type: ParticleParams + methods: + - name: apply + comment: Emit the particle system. Omit linkNumber to call ll.ParticleSystem, or pass a link number for ll.LinkParticleSystem. + parameters: + - name: self + type: ParticleParams + - name: linkNumber + type: number? + return-type: '()' - name: PrimParamsSetterType comment: Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast instance-type: '{any}' From 1f65fffd9f1da3f5f3bf154dd312ee182278a904 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 16 Jun 2026 20:30:30 +0000 Subject: [PATCH 03/12] Rework fluent in simple tables with unwrap when passed to the function --- generated/cpp/fluent_builder_descriptors.cpp | 47 ++++-- generated/experimental/enums.txt | 10 +- generated/lua_keywords_pretty.xml | 42 ++++- generated/secondlife.d.luau | 87 +++++----- generated/secondlife.docs.json | 3 + generated/secondlife_selene.yml | 139 +--------------- lsl_definitions.schema.json | 17 +- lsl_definitions.yaml | 20 ++- lsl_definitions/generators/lscript.py | 164 ++++++++++++++++--- lsl_definitions/lsl.py | 3 + lsl_definitions/rulesets.py | 32 +++- lsl_definitions/slua.py | 59 ++++--- slua_definitions.yaml | 35 ++-- 13 files changed, 369 insertions(+), 289 deletions(-) diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp index 681ea513..8402c7ee 100644 --- a/generated/cpp/fluent_builder_descriptors.cpp +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -1,17 +1,17 @@ // particle-params static const FluentParamDescriptor kParticleParamsDescs[] = { {"flags", 'i', 0}, - {"start_color", 'v', 1}, - {"start_alpha", 'f', 2}, - {"end_color", 'v', 3}, - {"end_alpha", 'f', 4}, - {"start_scale", 'v', 5}, - {"end_scale", 'v', 6}, + {"color_begin", 'v', 1}, + {"alpha_begin", 'f', 2}, + {"color_end", 'v', 3}, + {"alpha_end", 'f', 4}, + {"scale_begin", 'v', 5}, + {"scale_end", 'v', 6}, {"part_max_age", 'f', 7}, {"accel", 'v', 8}, {"pattern", 'i', 9}, - {"innerangle", 'f', 10}, - {"outerangle", 'f', 11}, + {"angle_inner", 'f', 10}, + {"angle_outer", 'f', 11}, {"texture", 'a', 12}, {"burst_rate", 'f', 13}, {"burst_count", 'i', 15}, @@ -25,16 +25,12 @@ static const FluentParamDescriptor kParticleParamsDescs[] = { {"angle_end", 'f', 23}, {"blend_func_source", 'i', 24}, {"blend_func_dest", 'i', 25}, - {"start_glow", 'f', 26}, - {"end_glow", 'f', 27}, + {"glow_begin", 'f', 26}, + {"glow_end", 'f', 27}, }; -static FluentBuilderDef* kParticleParamsDef = fluent_builder_def_build( - "ParticleSystem", "LinkParticleSystem", - kParticleParamsDescs, std::size(kParticleParamsDescs)); -slua_open_fluent_builder(L, "llparticle", "ParticleParams", kParticleParamsDef); static const FluentFlagDescriptor kParticleParamFlagDescs[] = { - {"interp_color", 0x1, 0}, - {"interp_scale", 0x2, 0}, + {"color_interp", 0x1, 0}, + {"scale_interp", 0x2, 0}, {"bounce", 0x4, 0}, {"wind", 0x8, 0}, {"follow", 0x10, 0}, @@ -44,4 +40,21 @@ static const FluentFlagDescriptor kParticleParamFlagDescs[] = { {"emissive", 0x100, 0}, {"ribbon", 0x400, 0}, }; -fluent_builder_def_add_flags(kParticleParamsDef, kParticleParamFlagDescs, std::size(kParticleParamFlagDescs)); \ No newline at end of file +static FluentBuilderDef* kParticleParamsDef = []() { + auto* d = fluent_builder_def_build(kParticleParamsDescs, std::size(kParticleParamsDescs)); + fluent_builder_def_add_flags(d, kParticleParamFlagDescs, std::size(kParticleParamFlagDescs)); + return d; +}(); +auto particle_system = [](lua_State* L) -> int { + const auto* def = (const FluentBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1)); + int link = lua_isnoneornil(L, 2) ? SLUA_LINK_THIS : luaL_checkinteger(L, 2); + slua_fluent_serialize(L, 1, def); + int rules_idx = lua_gettop(L); + lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "ll"); + lua_rawgetfield(L, -1, "LinkParticleSystem"); + lua_pushinteger(L, link); + lua_pushvalue(L, rules_idx); + lua_call(L, 2, 0); + return 0; +}; +slua_register_fluent_fn(L, "llprim", "ParticleSystem", particle_system, kParticleParamsDef); \ No newline at end of file diff --git a/generated/experimental/enums.txt b/generated/experimental/enums.txt index f4122b62..27e3f4f4 100644 --- a/generated/experimental/enums.txt +++ b/generated/experimental/enums.txt @@ -814,11 +814,11 @@ enum ParticleParam { PART_END_GLOW = 27 }; enum ParticleSourcePattern { - DROP = 0x1 - EXPLODE = 0x2 - ANGLE = 0x4 - ANGLE_CONE = 0x8 - ANGLE_CONE_EMPTY = 0x10 + DROP = 1 + EXPLODE = 2 + ANGLE = 4 + ANGLE_CONE = 8 + ANGLE_CONE_EMPTY = 16 }; enum PassMode { IF_NOT_HANDLED = 0 diff --git a/generated/lua_keywords_pretty.xml b/generated/lua_keywords_pretty.xml index ccaa5255..06aad683 100644 --- a/generated/lua_keywords_pretty.xml +++ b/generated/lua_keywords_pretty.xml @@ -198,6 +198,11 @@ export type DateTypeResult = { isdst: boolean?, } + ParticleParams + + tooltip + Particle system parameter table. Pass to llprim.ParticleSystem() to emit particles. + list tooltip @@ -219,11 +224,6 @@ export type rotation = quaternion tooltip Timer management class for scheduling periodic and one-time callbacks - ParticleParams - - tooltip - Particle system parameter table. Set properties and call apply() to emit particles. - PrimParamsSetterType tooltip @@ -13364,6 +13364,38 @@ Returns true if result is non-zero. tooltip Utilities for working with prims / objects + llprim.ParticleSystem + + arguments + + + params + + tooltip + + type + ParticleParams? + + + + link + + tooltip + + type + number? + + + + energy + 10.0 + return + () + sleep + 0.0 + tooltip + Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. + math energy diff --git a/generated/secondlife.d.luau b/generated/secondlife.d.luau index 1b65c167..a3e9d2b9 100644 --- a/generated/secondlife.d.luau +++ b/generated/secondlife.d.luau @@ -85,6 +85,45 @@ type LLJsonDecodeOptionsWithPath = { reviver: (key: string | number, value: any, parent: {}?, ctx: {path: {string | number}}) -> any } type LLJsonDecodeOptions = LLJsonDecodeOptionsWithoutPath | LLJsonDecodeOptionsWithPath +export type ParticleParams = { + flags: number?, + color_begin: vector?, + alpha_begin: number?, + color_end: vector?, + alpha_end: number?, + scale_begin: vector?, + scale_end: vector?, + part_max_age: number?, + accel: vector?, + pattern: number?, + angle_inner: number?, + angle_outer: number?, + texture: (string | uuid)?, + burst_rate: number?, + burst_count: number?, + burst_radius: number?, + burst_speed_min: number?, + burst_speed_max: number?, + src_max_age: number?, + target_key: (string | uuid)?, + omega: vector?, + angle_begin: number?, + angle_end: number?, + blend_func_source: number?, + blend_func_dest: number?, + glow_begin: number?, + glow_end: number?, + color_interp: boolean?, + scale_interp: boolean?, + bounce: boolean?, + wind: boolean?, + follow: boolean?, + follow_velocity: boolean?, + target_pos: boolean?, + target_linear: boolean?, + emissive: boolean?, + ribbon: boolean?, +} declare extern type DetectedEvent with index: number @@ -169,53 +208,6 @@ declare extern type LLTimers with function off(self, callback: LLTimerCallback): boolean end -export type ParticleParamsMeta = { - __index: ParticleParamsMeta, - flags: number?, - start_color: vector?, - start_alpha: number?, - end_color: vector?, - end_alpha: number?, - start_scale: vector?, - end_scale: vector?, - part_max_age: number?, - accel: vector?, - pattern: number?, - innerangle: number?, - outerangle: number?, - texture: (string | uuid)?, - burst_rate: number?, - burst_count: number?, - burst_radius: number?, - burst_speed_min: number?, - burst_speed_max: number?, - src_max_age: number?, - target_key: (string | uuid)?, - omega: vector?, - angle_begin: number?, - angle_end: number?, - blend_func_source: number?, - blend_func_dest: number?, - start_glow: number?, - end_glow: number?, - interp_color: boolean?, - interp_scale: boolean?, - bounce: boolean?, - wind: boolean?, - follow: boolean?, - follow_velocity: boolean?, - target_pos: boolean?, - target_linear: boolean?, - emissive: boolean?, - ribbon: boolean?, - new: (init: {[string]: any}?) -> ParticleParams, - apply: (self: ParticleParams, linkNumber: number?) -> (), -} - -export type ParticleParams = typeof( - setmetatable((nil :: any) :: {}, (nil :: any) :: ParticleParamsMeta) -) - export type PrimParamsSetterTypeMeta = { __index: PrimParamsSetterTypeMeta, new: () -> PrimParamsSetterType, @@ -432,6 +424,7 @@ declare lljson: { declare llprim: { ParamsSetter: PrimParamsSetterTypeMeta, + ParticleSystem: (params: ParticleParams?, link: number?) -> (), } diff --git a/generated/secondlife.docs.json b/generated/secondlife.docs.json index 4bdf9206..1cc8a81b 100644 --- a/generated/secondlife.docs.json +++ b/generated/secondlife.docs.json @@ -370,6 +370,9 @@ "@sl-slua/global/llprim.ParamsSetter": { "documentation": "Value: PrimParamsSetterTypeMeta
Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast" }, + "@sl-slua/global/llprim.ParticleSystem": { + "documentation": "Emit a particle system from a ParticleParams table. Omit link to apply to the current prim." + }, "@sl-slua/global/math": { "documentation": "Mathematical functions library.", "learn_more_link": "https://luau.org/library/#math-library" diff --git a/generated/secondlife_selene.yml b/generated/secondlife_selene.yml index 8d4a17bb..8a930083 100644 --- a/generated/secondlife_selene.yml +++ b/generated/secondlife_selene.yml @@ -5507,6 +5507,14 @@ globals: llprim.ParamsSetter: property: read-only description: Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast + llprim.ParticleSystem: + args: + - type: table + required: false + - type: number + required: false + description: Emit a particle system from a ParticleParams table. Omit link to + apply to the current prim. math.pi: property: read-only type: number @@ -16106,137 +16114,6 @@ structs: args: - type: function description: Unregisters a timer callback. Returns true if found and removed. - ParticleParams: - apply: - method: true - args: - - type: number - required: false - description: Emit the particle system. Omit linkNumber to call ll.ParticleSystem, - or pass a link number for ll.LinkParticleSystem. - new: - args: - - type: table - required: false - description: Create a new ParticleParams, optionally bulk-initializing from - a table. - flags: - property: full-write - type: number - start_color: - property: full-write - type: - display: vector - start_alpha: - property: full-write - type: number - end_color: - property: full-write - type: - display: vector - end_alpha: - property: full-write - type: number - start_scale: - property: full-write - type: - display: vector - end_scale: - property: full-write - type: - display: vector - part_max_age: - property: full-write - type: number - accel: - property: full-write - type: - display: vector - pattern: - property: full-write - type: number - innerangle: - property: full-write - type: number - outerangle: - property: full-write - type: number - texture: - property: full-write - type: string - burst_rate: - property: full-write - type: number - burst_count: - property: full-write - type: number - burst_radius: - property: full-write - type: number - burst_speed_min: - property: full-write - type: number - burst_speed_max: - property: full-write - type: number - src_max_age: - property: full-write - type: number - target_key: - property: full-write - type: string - omega: - property: full-write - type: - display: vector - angle_begin: - property: full-write - type: number - angle_end: - property: full-write - type: number - blend_func_source: - property: full-write - type: number - blend_func_dest: - property: full-write - type: number - start_glow: - property: full-write - type: number - end_glow: - property: full-write - type: number - interp_color: - property: full-write - type: bool - interp_scale: - property: full-write - type: bool - bounce: - property: full-write - type: bool - wind: - property: full-write - type: bool - follow: - property: full-write - type: bool - follow_velocity: - property: full-write - type: bool - target_pos: - property: full-write - type: bool - target_linear: - property: full-write - type: bool - emissive: - property: full-write - type: bool - ribbon: - property: full-write - type: bool PrimParamsSetterType: apply: method: true diff --git a/lsl_definitions.schema.json b/lsl_definitions.schema.json index 8966c65e..dcde0d36 100644 --- a/lsl_definitions.schema.json +++ b/lsl_definitions.schema.json @@ -264,13 +264,13 @@ "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$" }, - "apply-fn": { - "markdownDescription": "(`table` only) `ll.*` function called by `apply()` with no link number (e.g. `ParticleSystem`).", + "lua-fn": { + "markdownDescription": "(`table` only) Lua function name exposed in `lua-module` (e.g. `ParticleSystem`).", "type": "string", - "pattern": "^[A-Z][A-Za-z0-9]*$" + "pattern": "^[A-Za-z][a-zA-Z0-9]*$" }, - "apply-link-fn": { - "markdownDescription": "(`table` only) `ll.*` function called by `apply(linkNumber)` (e.g. `LinkParticleSystem`).", + "dispatch-fn": { + "markdownDescription": "(`table` only) `ll.*` function name whose signature drives code generation (e.g. `LinkParticleSystem`). Must take a list as its last argument.", "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$" } @@ -280,7 +280,7 @@ "required": ["type"] }, "then": { "required": ["rules"] }, - "else": { "required": ["lua-module", "lua-type", "apply-fn", "apply-link-fn"] } + "else": { "required": ["lua-module", "lua-type", "lua-fn", "dispatch-fn"] } } }, "additionalProperties": false, @@ -357,6 +357,11 @@ "value-type": { "markdownDescription": "For constants used as rule tags in list-based APIs: the type of the value that follows this constant in the list. Uses the same vocabulary as ruleset param types.", "enum": ["integer", "float", "string", "vector", "rotation", "boolean", "asset", "key"] + }, + "pretty-name": { + "markdownDescription": "Override the auto-generated property alias for this constant when used as a table-ruleset key. Must be a valid Lua identifier (lowercase, underscores).", + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" } }, "required": [ diff --git a/lsl_definitions.yaml b/lsl_definitions.yaml index d145d314..9acdf238 100644 --- a/lsl_definitions.yaml +++ b/lsl_definitions.yaml @@ -326,7 +326,7 @@ enums: prefix: PSYS_ tooltip: '' ParticleSourcePattern: - type: flag + type: enum prefix: PSYS_SRC_PATTERN_ tooltip: '' PassMode: @@ -4598,6 +4598,7 @@ constants: value: '0x100' PSYS_PART_END_ALPHA: member-of: [ParticleParam] + pretty-name: alpha_end tooltip: Float parameter that determines the ending alpha (transparency) of the particles. type: integer @@ -4605,6 +4606,7 @@ constants: value-type: float PSYS_PART_END_COLOR: member-of: [ParticleParam] + pretty-name: color_end tooltip: Vector parameter that determines the ending color of the particles. type: integer @@ -4612,12 +4614,14 @@ constants: value-type: vector PSYS_PART_END_GLOW: member-of: [ParticleParam] + pretty-name: glow_end tooltip: Float parameter that determines the ending glow value of the particles. type: integer value: 27 value-type: float PSYS_PART_END_SCALE: member-of: [ParticleParam] + pretty-name: scale_end tooltip: Vector parameter defining the ending size of the particle billboard in meters (z-axis value is ignored). type: integer @@ -4646,12 +4650,14 @@ constants: value: '0x20' PSYS_PART_INTERP_COLOR_MASK: member-of: [ParticleFlag] + pretty-name: color_interp tooltip: Particle flag causing the particle's color and alpha to smoothly interpolate from their START values to their END values over its lifetime. type: integer value: '0x1' PSYS_PART_INTERP_SCALE_MASK: member-of: [ParticleFlag] + pretty-name: scale_interp tooltip: Particle flag causing the particle's size/scale to transition from its START setting to its END setting over its lifetime. type: integer @@ -4674,6 +4680,7 @@ constants: value: '0x400' PSYS_PART_START_ALPHA: member-of: [ParticleParam] + pretty-name: alpha_begin tooltip: Float parameter that determines the starting alpha (transparency) of the particles. type: integer @@ -4681,6 +4688,7 @@ constants: value-type: float PSYS_PART_START_COLOR: member-of: [ParticleParam] + pretty-name: color_begin tooltip: Vector parameter that determines the starting color of the particles. type: integer @@ -4688,12 +4696,14 @@ constants: value-type: vector PSYS_PART_START_GLOW: member-of: [ParticleParam] + pretty-name: glow_begin tooltip: Float parameter that determines the starting glow value of the particles. type: integer value: 26 value-type: float PSYS_PART_START_SCALE: member-of: [ParticleParam] + pretty-name: scale_begin tooltip: Vector parameter defining the starting size of the particle billboard in meters (z-axis value is ignored). type: integer @@ -4778,6 +4788,7 @@ constants: value-type: float PSYS_SRC_INNERANGLE: member-of: [ParticleParam] + pretty-name: angle_inner tooltip: Float parameter specifying the inner angle of the arc for ANGLE or ANGLE_CONE patterns. The specified inner area will not contain particles. type: integer @@ -4807,6 +4818,7 @@ constants: value-type: vector PSYS_SRC_OUTERANGLE: member-of: [ParticleParam] + pretty-name: angle_outer tooltip: Float parameter specifying the outer angle of the arc for ANGLE or ANGLE_CONE patterns. The area between the outer and inner angles is filled with particles. @@ -16979,7 +16991,7 @@ builder-rulesets: flag-enum: ParticleFlag flag-field: flags flag-mask: _MASK - lua-module: llparticle + lua-module: llprim + lua-fn: ParticleSystem lua-type: ParticleParams - apply-fn: ParticleSystem - apply-link-fn: LinkParticleSystem + dispatch-fn: LinkParticleSystem diff --git a/lsl_definitions/generators/lscript.py b/lsl_definitions/generators/lscript.py index a0cff08c..d732ec70 100644 --- a/lsl_definitions/generators/lscript.py +++ b/lsl_definitions/generators/lscript.py @@ -886,11 +886,16 @@ def gen_lua_constant_definitions(definitions: LSLDefinitions) -> str: @register("gen_fluent_builder_descriptors") def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: - """Generate FluentParamDescriptor arrays and init calls for all type:table builder-rulesets. - - Reads each ruleset's metadata (enum, filler-tokens, apply-fn, apply-link-fn, - lua-module, lua-type) from lsl_definitions.yaml. The resulting C++ fragment is - meant to be #include'd directly inside init_fluent_builders(lua_State* L). + """Generate FluentParamDescriptor arrays and dispatch wrappers for all type:table rulesets. + + For each ruleset, inspects the signature of ll{dispatch-fn} to determine whether + a link parameter is needed, then emits: + - FluentParamDescriptor array + - FluentFlagDescriptor array (if flag-enum present) + - fluent_builder_def_build / fluent_builder_def_add_flags calls (as a static initializer) + - a lambda dispatch wrapper + - a slua_register_fluent_fn call + The resulting fragment is #include'd inside init_fluent_builders(lua_State* L). """ _semantic_map = { "integer": "i", @@ -903,6 +908,100 @@ def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: "key": "k", } + def _inspect_dispatch_fn(dispatch_fn_name: str): + """Inspect ll{dispatch_fn_name}'s argument list. + + Returns (prefix_args, has_link) where prefix_args is a list of + (name: str, is_link: bool) tuples for each non-list prefix arg in LL order. + Raises ValueError for unsupported signatures (non-integer prefix args, + or list not last). + """ + full_name = "ll" + dispatch_fn_name + func = definitions.functions.get(full_name) + if func is None: + raise ValueError(f"dispatch-fn '{full_name}' not found in definitions") + + args = func.arguments + if not args or args[-1].type != LSLType.LIST: + last = args[-1].type if args else "no args" + raise ValueError(f"{full_name}: last argument must be list, got {last}") + + prefix_args = args[:-1] + result = [] + has_link = False + for arg in prefix_args: + if arg.type != LSLType.INTEGER: + raise ValueError( + f"{full_name}: unsupported non-integer prefix arg '{arg.name}' " + f"(type {arg.type}). Only integer prefix args are supported; " + f"hand-write this wrapper using slua_fluent_serialize() directly." + ) + is_link = "link" in arg.name.lower() + if is_link: + has_link = True + result.append((arg.name, is_link)) + return result, has_link + + def _lua_fn_to_cpp_name(lua_fn: str) -> str: + """Convert camelCase Lua function name to snake_case C++ variable name. + e.g. particleSystem -> particle_system + """ + return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", lua_fn).lower() + + def _build_wrapper(cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: bool) -> str: + """Emit a C++ lambda that reads Lua args, serializes params, and calls ll.dispatch_fn. + + Lua stack layout: + 1 .. N : non-link integer args (in LL order, skipping link) + N+1 : params table + N+2 : link (optional, if has_link) + LL call order: original LL order (link, non-link args, rules list). + """ + non_link_count = sum(1 for _, is_link in prefix_args if not is_link) + params_lua_pos = non_link_count + 1 + link_lua_pos = params_lua_pos + 1 + total_ll_args = len(prefix_args) + 1 # prefix args + rules list + + lines = [] + + # Read non-link args from Lua stack (positions 1..N, in LL order). + lua_pos = 1 + for arg_name, is_link in prefix_args: + if not is_link: + lines.append(f" int {arg_name.lower()} = luaL_checkinteger(L, {lua_pos});") + lua_pos += 1 + + # Read optional link arg (last Lua position). + if has_link: + lines.append( + f" int link = lua_isnoneornil(L, {link_lua_pos}) " + f"? SLUA_LINK_THIS : luaL_checkinteger(L, {link_lua_pos});" + ) + + # Serialize params table into a flat rules list. + lines.append(f" slua_fluent_serialize(L, {params_lua_pos}, def);") + lines.append(" int rules_idx = lua_gettop(L);") + + # Dispatch to ll.* in original LL order. + lines.append(' lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "ll");') + lines.append(f' lua_rawgetfield(L, -1, "{dispatch_fn}");') + for arg_name, is_link in prefix_args: + if is_link: + lines.append(" lua_pushinteger(L, link);") + else: + lines.append(f" lua_pushinteger(L, {arg_name.lower()});") + lines.append(" lua_pushvalue(L, rules_idx);") + lines.append(f" lua_call(L, {total_ll_args}, 0);") + lines.append(" return 0;") + + body = "\n".join(lines) + return ( + f"auto {cpp_name} = [](lua_State* L) -> int {{\n" + f" const auto* def = (const FluentBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1));\n" + f"{body}\n" + f"}};" + ) + sections = [] for ruleset_name, ruleset_data in definitions.builder_rulesets.items(): if ruleset_data.get("type", "builder") != "table": @@ -910,9 +1009,11 @@ def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: enum_name = ruleset_data["enum"] lua_module = ruleset_data["lua-module"] - lua_type = ruleset_data["lua-type"] - apply_fn = ruleset_data["apply-fn"] - apply_link_fn = ruleset_data["apply-link-fn"] + lua_fn = ruleset_data["lua-fn"] + dispatch_fn = ruleset_data["dispatch-fn"] + + prefix_args, has_link = _inspect_dispatch_fn(dispatch_fn) + cpp_name = _lua_fn_to_cpp_name(lua_fn) # Descriptor array/def names derived from the enum name. # e.g. ParticleParam -> kParticleParamsDescs, kParticleParamsDef @@ -925,14 +1026,10 @@ def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: sem = _semantic_map[desc.value_type] desc_lines.append(f" {{\"{name}\", '{sem}', {desc.tag}}},") - body = "\n".join(desc_lines) + param_body = "\n".join(desc_lines) section = ( f"// {ruleset_name}\n" - f"static const FluentParamDescriptor {array_name}[] = {{\n{body}\n}};\n" - f"static FluentBuilderDef* {def_name} = fluent_builder_def_build(\n" - f' "{apply_fn}", "{apply_link_fn}",\n' - f" {array_name}, std::size({array_name}));\n" - f'slua_open_fluent_builder(L, "{lua_module}", "{lua_type}", {def_name});' + f"static const FluentParamDescriptor {array_name}[] = {{\n{param_body}\n}};\n" ) flag_enum_name = ruleset_data.get("flag-enum") @@ -965,25 +1062,46 @@ def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: ) flag_suffix = (ruleset_data.get("flag-mask") or "").lower() - flag_array_name = f"k{enum_name}FlagDescs" flag_lines = [] for const in flag_consts: - name = const.name - strict = (name[len(prefix) :] if name.startswith(prefix) else name).lower() - if flag_suffix and strict.endswith(flag_suffix): - strict = strict[: -len(flag_suffix)] - tokens = [t for t in strict.split("_") if t not in filler_tokens] - prop_name = "_".join(tokens) if tokens else strict + if const.pretty_name: + prop_name = const.pretty_name + else: + name = const.name + strict = (name[len(prefix) :] if name.startswith(prefix) else name).lower() + if flag_suffix and strict.endswith(flag_suffix): + strict = strict[: -len(flag_suffix)] + tokens = [t for t in strict.split("_") if t not in filler_tokens] + prop_name = "_".join(tokens) if tokens else strict mask = int(const.value, 0) flag_lines.append(f' {{"{prop_name}", 0x{mask:x}, {field_tag}}},') flag_body = "\n".join(flag_lines) section += ( - f"\nstatic const FluentFlagDescriptor {flag_array_name}[] = {{\n{flag_body}\n}};\n" - f"fluent_builder_def_add_flags({def_name}, {flag_array_name}, std::size({flag_array_name}));" + f"static const FluentFlagDescriptor {flag_array_name}[] = {{\n{flag_body}\n}};\n" ) + # Build def and add flags in a single static initializer so it is safe + # to call init_fluent_builders more than once. + section += ( + f"static FluentBuilderDef* {def_name} = []() {{\n" + f" auto* d = fluent_builder_def_build({array_name}, std::size({array_name}));\n" + f" fluent_builder_def_add_flags(d, {flag_array_name}, std::size({flag_array_name}));\n" + f" return d;\n" + f"}}();\n" + ) + else: + section += ( + f"static FluentBuilderDef* {def_name} = " + f"fluent_builder_def_build({array_name}, std::size({array_name}));\n" + ) + + section += _build_wrapper(cpp_name, dispatch_fn, prefix_args, has_link) + "\n" + section += ( + f'slua_register_fluent_fn(L, "{lua_module}", "{lua_fn}", {cpp_name}, {def_name});' + ) + sections.append(section) return "\n\n".join(sections) diff --git a/lsl_definitions/lsl.py b/lsl_definitions/lsl.py index 58519bb6..291076d0 100644 --- a/lsl_definitions/lsl.py +++ b/lsl_definitions/lsl.py @@ -152,6 +152,8 @@ class LSLConstant: slua_deprecated: Deprecated | None private: bool """Whether this should this be included in the syntax file""" + pretty_name: Optional[str] = None + """Optional override for the auto-generated property alias in table-ruleset APIs.""" member_of: list[LSLEnum] = dataclasses.field(default_factory=list) value_type: Optional[str] = None @@ -785,6 +787,7 @@ def _handle_constant(self, const_name: str, const_data: dict) -> LSLConstant: const_data.get("slua-deprecated", False) ), value_type=const_data.get("value-type", None), + pretty_name=const_data.get("pretty-name", None), ) if const.type not in {"float", "integer", "string", "vector", "rotation"}: raise ValueError(f"Invalid constant type {const.type}") diff --git a/lsl_definitions/rulesets.py b/lsl_definitions/rulesets.py index 2096e0d2..59ceebd1 100644 --- a/lsl_definitions/rulesets.py +++ b/lsl_definitions/rulesets.py @@ -180,7 +180,7 @@ def expand_member_params( f"Constant {const.name!r} is a member of {enum_name!r} " f"but has no value-type annotation." ) - rows.append((strict, int(const.value, 0), const.value_type)) + rows.append((strict, int(const.value, 0), const.value_type, const.pretty_name)) # Hard-fail on strict name collisions (implies a data error). strict_names = [r[0] for r in rows] @@ -189,21 +189,41 @@ def expand_member_params( raise ValueError(f"Strict name collision(s) in {enum_name}: {sorted(dupes)}") # Pass 2: pretty aliases. - def _pretty(strict: str) -> str: + # A constant's pretty_name field takes priority over the filler-token derivation. + def _pretty(strict: str, override: Optional[str]) -> str: + if override is not None: + return override tokens = [t for t in strict.split("_") if t not in filler_tokens] return "_".join(tokens) if tokens else strict - pretties = [_pretty(strict) for strict, _, _ in rows] - colliding = {p for p in pretties if pretties.count(p) > 1} + pretties = [_pretty(strict, override) for strict, _, _, override in rows] + + # Hard-fail if any pretty name is duplicated and at least one side is an explicit + # override (author error — cannot silently revert an intentional name). + pretty_counts: dict[str, int] = {} + for p in pretties: + pretty_counts[p] = pretty_counts.get(p, 0) + 1 + for (_, _, _, override), p in zip(rows, pretties): + if pretty_counts[p] > 1 and override is not None: + raise ValueError( + f"pretty-name collision in {enum_name}: '{p}' is used by multiple constants" + ) + + # Auto-derived names that collide revert to strict names instead of erroring. + colliding = { + p + for (_, _, _, override), p in zip(rows, pretties) + if override is None and pretty_counts[p] > 1 + } return [ MemberDescriptor( strict_name=strict, - pretty_name=None if pretty in colliding else pretty, + pretty_name=None if (override is None and pretty in colliding) else pretty, tag=tag, value_type=vt, ) - for (strict, tag, vt), pretty in zip(rows, pretties) + for (strict, tag, vt, override), pretty in zip(rows, pretties) ] diff --git a/lsl_definitions/slua.py b/lsl_definitions/slua.py index 8639550c..7bd4a764 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -668,10 +668,11 @@ def make_fluent_method(builder_spec: BuilderSpec, method: BuilderMethod) -> SLua builder_class.methods.extend(methods) def _generate_fluent_builder_classes(self, lsl: "LSLDefinitions") -> None: - """Inject typed properties into fluent builder classes declared in slua_definitions.yaml. + """Inject typed properties into fluent builder classes or type aliases. For each table-type builder ruleset that names a lua-type, finds the matching - SLuaClassDeclaration in self.classes and populates its properties list from + SLuaClassDeclaration in self.classes (and populates its properties list) OR a + SLuaTypeAlias in self.type_aliases (and rebuilds its definition string) from expand_table_ruleset() plus any flag enum members. """ for ruleset_name, ruleset_data in lsl.builder_rulesets.items(): @@ -680,22 +681,20 @@ def _generate_fluent_builder_classes(self, lsl: "LSLDefinitions") -> None: lua_type = ruleset_data.get("lua-type") if not lua_type: continue - try: - cls = next(c for c in self.classes if c.name == lua_type) - except StopIteration: + + cls = next((c for c in self.classes if c.name == lua_type), None) + alias = next((a for a in self.type_aliases if a.name == lua_type), None) + if cls is None and alias is None: continue + # Collect (prop_name, luau_type) pairs + props: list[tuple[str, str]] = [] + # Regular properties from expand_table_ruleset for desc in expand_table_ruleset(lsl, ruleset_name): prop_name = desc.pretty_name if desc.pretty_name else desc.strict_name luau_base = _TABLE_RULESET_TYPE_MAP.get(desc.value_type, "any") - cls.properties.append( - SLuaProperty( - name=prop_name, - type=f"{luau_base}?", - modifiable="full-write", - ) - ) + props.append((prop_name, f"{luau_base}?")) # Flag enum boolean properties flag_enum_name = ruleset_data.get("flag-enum") @@ -713,20 +712,32 @@ def _generate_fluent_builder_classes(self, lsl: "LSLDefinitions") -> None: key=lambda c: int(c.value, 0), ) for const in flag_consts: - strict = ( - const.name[len(prefix) :] if const.name.startswith(prefix) else const.name - ).lower() - if flag_suffix and strict.endswith(flag_suffix): - strict = strict[: -len(flag_suffix)] - tokens = [t for t in strict.split("_") if t not in filler_tokens] - prop_name = "_".join(tokens) if tokens else strict + if const.pretty_name: + prop_name = const.pretty_name + else: + strict = ( + const.name[len(prefix) :] + if const.name.startswith(prefix) + else const.name + ).lower() + if flag_suffix and strict.endswith(flag_suffix): + strict = strict[: -len(flag_suffix)] + tokens = [t for t in strict.split("_") if t not in filler_tokens] + prop_name = "_".join(tokens) if tokens else strict + props.append((prop_name, "boolean?")) + + if cls is not None: + for prop_name, prop_type in props: cls.properties.append( - SLuaProperty( - name=prop_name, - type="boolean?", - modifiable="full-write", - ) + SLuaProperty(name=prop_name, type=prop_type, modifiable="full-write") ) + else: + # Rebuild the type alias definition as a typed table literal. + lines = ["{"] + for prop_name, prop_type in props: + lines.append(f" {prop_name}: {prop_type},") + lines.append("}") + alias.definition = "\n".join(lines) class SLuaDefinitionParser: diff --git a/slua_definitions.yaml b/slua_definitions.yaml index 64d6522b..2e85f773 100644 --- a/slua_definitions.yaml +++ b/slua_definitions.yaml @@ -237,6 +237,11 @@ type-aliases: comment: Configuration options for lljson decoding definition: "LLJsonDecodeOptionsWithoutPath | LLJsonDecodeOptionsWithPath" selene-type: any +- name: ParticleParams + comment: Particle system parameter table. Pass to llprim.ParticleSystem() to emit particles. + export: true + definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("particle-params") + selene-type: table classes: - name: LLEvents comment: Event registration and management class for Second Life events @@ -317,27 +322,6 @@ classes: - name: callback type: LLTimerCallback return-type: boolean -- name: ParticleParams - comment: Particle system parameter table. Set properties and call apply() to emit particles. - instance-type: '{}' - export: true - properties: [] # injected at generation time from expand_table_ruleset("particle-params") - functions: - - name: new - comment: Create a new ParticleParams, optionally bulk-initializing from a table. - parameters: - - name: init - type: '{[string]: any}?' - return-type: ParticleParams - methods: - - name: apply - comment: Emit the particle system. Omit linkNumber to call ll.ParticleSystem, or pass a link number for ll.LinkParticleSystem. - parameters: - - name: self - type: ParticleParams - - name: linkNumber - type: number? - return-type: '()' - name: PrimParamsSetterType comment: Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast instance-type: '{any}' @@ -1473,6 +1457,15 @@ modules: comment: Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast type: PrimParamsSetterTypeMeta value: PrimParamsSetterTypeMeta + functions: + - name: ParticleSystem + comment: Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. + parameters: + - name: params + type: ParticleParams? + - name: link + type: number? + return-type: '()' - name: math comment: Mathematical functions library. functions: From 0aadd719e946cec97ef2355ee52efe50bb7b17ac Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 17 Jun 2026 18:18:22 +0000 Subject: [PATCH 04/12] Adding media parameters and correct casing on methods. --- gen_all_definitions.sh | 2 +- generated/cpp/fluent_builder_descriptors.cpp | 37 +++++++++++++++- generated/lua_keywords_pretty.xml | 46 ++++++++++++++++++++ generated/secondlife.d.luau | 18 ++++++++ generated/secondlife.docs.json | 3 ++ generated/secondlife_selene.yml | 9 ++++ generated/syntax/slua.tmLanguage | 4 +- generated/syntax/slua.tmLanguage.json | 4 +- lsl_definitions.yaml | 25 ++++++++++- lsl_definitions/slua.py | 3 +- slua_definitions.yaml | 17 +++++++- 11 files changed, 159 insertions(+), 9 deletions(-) diff --git a/gen_all_definitions.sh b/gen_all_definitions.sh index 424db987..241b889f 100755 --- a/gen_all_definitions.sh +++ b/gen_all_definitions.sh @@ -7,7 +7,7 @@ mkdir -p "$outdir" "$outdir/cpp" "$outdir/templated" "$outdir/experimental" "$ou DEFS="./lsl_definitions.yaml" SLUA="./slua_definitions.yaml" -CMD="python gen_definitions.py" +CMD="$(command -v python3 || command -v python) gen_definitions.py" # Viewer outputs $CMD $DEFS syntax "$outdir/lsl_keywords.xml" diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp index 8402c7ee..1e4a5979 100644 --- a/generated/cpp/fluent_builder_descriptors.cpp +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -57,4 +57,39 @@ auto particle_system = [](lua_State* L) -> int { lua_call(L, 2, 0); return 0; }; -slua_register_fluent_fn(L, "llprim", "ParticleSystem", particle_system, kParticleParamsDef); \ No newline at end of file +slua_register_fluent_fn(L, "llprim", "ParticleSystem", particle_system, kParticleParamsDef); + +// prim-media-params +static const FluentParamDescriptor kPrimMediaParamsDescs[] = { + {"alt_image_enable", 'b', 0}, + {"controls", 'i', 1}, + {"current_url", 's', 2}, + {"home_url", 's', 3}, + {"auto_loop", 'b', 4}, + {"auto_play", 'b', 5}, + {"auto_scale", 'b', 6}, + {"auto_zoom", 'b', 7}, + {"first_click_interact", 'b', 8}, + {"width_pixels", 'i', 9}, + {"height_pixels", 'i', 10}, + {"whitelist_enable", 'b', 11}, + {"whitelist", 's', 12}, + {"perms_interact", 'i', 13}, + {"perms_control", 'i', 14}, +}; +static FluentBuilderDef* kPrimMediaParamsDef = fluent_builder_def_build(kPrimMediaParamsDescs, std::size(kPrimMediaParamsDescs)); +auto set_media = [](lua_State* L) -> int { + const auto* def = (const FluentBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1)); + int face = luaL_checkinteger(L, 1); + int link = lua_isnoneornil(L, 3) ? SLUA_LINK_THIS : luaL_checkinteger(L, 3); + slua_fluent_serialize(L, 2, def); + int rules_idx = lua_gettop(L); + lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "ll"); + lua_rawgetfield(L, -1, "SetLinkMedia"); + lua_pushinteger(L, link); + lua_pushinteger(L, face); + lua_pushvalue(L, rules_idx); + lua_call(L, 3, 0); + return 0; +}; +slua_register_fluent_fn(L, "llprim", "SetMedia", set_media, kPrimMediaParamsDef); \ No newline at end of file diff --git a/generated/lua_keywords_pretty.xml b/generated/lua_keywords_pretty.xml index 06aad683..a8f590e2 100644 --- a/generated/lua_keywords_pretty.xml +++ b/generated/lua_keywords_pretty.xml @@ -198,6 +198,11 @@ export type DateTypeResult = { isdst: boolean?, }
+ MediaParams + + tooltip + Prim face media parameter table. Pass to llprim.SetMedia() to configure media on a face. + ParticleParams tooltip @@ -13396,6 +13401,47 @@ Returns true if result is non-zero. tooltip Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. + llprim.SetMedia + + arguments + + + face + + tooltip + + type + number + + + + params + + tooltip + + type + MediaParams? + + + + link + + tooltip + + type + number? + + + + energy + 10.0 + return + number + sleep + 0.0 + tooltip + Configure media on a specific face. Omit link to apply to the current prim. + math energy diff --git a/generated/secondlife.d.luau b/generated/secondlife.d.luau index a3e9d2b9..b712839c 100644 --- a/generated/secondlife.d.luau +++ b/generated/secondlife.d.luau @@ -124,6 +124,23 @@ export type ParticleParams = { emissive: boolean?, ribbon: boolean?, } +export type MediaParams = { + alt_image_enable: boolean?, + controls: number?, + current_url: string?, + home_url: string?, + auto_loop: boolean?, + auto_play: boolean?, + auto_scale: boolean?, + auto_zoom: boolean?, + first_click_interact: boolean?, + width_pixels: number?, + height_pixels: number?, + whitelist_enable: boolean?, + whitelist: string?, + perms_interact: number?, + perms_control: number?, +} declare extern type DetectedEvent with index: number @@ -425,6 +442,7 @@ declare lljson: { declare llprim: { ParamsSetter: PrimParamsSetterTypeMeta, ParticleSystem: (params: ParticleParams?, link: number?) -> (), + SetMedia: (face: number, params: MediaParams?, link: number?) -> number, } diff --git a/generated/secondlife.docs.json b/generated/secondlife.docs.json index 1cc8a81b..84ec0fa2 100644 --- a/generated/secondlife.docs.json +++ b/generated/secondlife.docs.json @@ -373,6 +373,9 @@ "@sl-slua/global/llprim.ParticleSystem": { "documentation": "Emit a particle system from a ParticleParams table. Omit link to apply to the current prim." }, + "@sl-slua/global/llprim.SetMedia": { + "documentation": "Configure media on a specific face. Omit link to apply to the current prim." + }, "@sl-slua/global/math": { "documentation": "Mathematical functions library.", "learn_more_link": "https://luau.org/library/#math-library" diff --git a/generated/secondlife_selene.yml b/generated/secondlife_selene.yml index 8a930083..33693292 100644 --- a/generated/secondlife_selene.yml +++ b/generated/secondlife_selene.yml @@ -5515,6 +5515,15 @@ globals: required: false description: Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. + llprim.SetMedia: + args: + - type: number + - type: table + required: false + - type: number + required: false + description: Configure media on a specific face. Omit link to apply to the current + prim. math.pi: property: read-only type: number diff --git a/generated/syntax/slua.tmLanguage b/generated/syntax/slua.tmLanguage index cb1975f0..6ff66913 100644 --- a/generated/syntax/slua.tmLanguage +++ b/generated/syntax/slua.tmLanguage @@ -621,7 +621,7 @@ name support.function.luau match - (?<![^.]\.|:)\b(bit32\.(?:arshift|band|bnot|bor|btest|bxor|byteswap|countlz|countrz|extract|lrotate|lshift|replace|rrotate|rshift|s32|smul)|buffer\.(?:copy|create|fill|fromstring|len|readbits|readf32|readf64|readi16|readi32|readi8|readstring|readu16|readu32|readu8|tostring|writebits|writef32|writef64|writei16|writei32|writei8|writestring|writeu16|writeu32|writeu8)|coroutine\.(?:close|create|isyieldable|resume|running|status|wrap|yield)|debug\.(?:info|traceback)|llbase64\.(?:decode|encode)|lljson\.(?:decode|encode|sldecode|slencode)|math\.(?:abs|acos|asin|atan|atan2|ceil|clamp|cos|cosh|deg|exp|floor|fmod|frexp|isfinite|isinf|isnan|ldexp|lerp|log|log10|map|max|min|modf|noise|pow|rad|random|randomseed|round|sign|sin|sinh|sqrt|tan|tanh)|os\.(?:clock|date|difftime|time)|quaternion\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|rotation\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|string\.(?:byte|char|find|format|gmatch|gsub|len|lower|match|pack|packsize|rep|reverse|split|sub|unpack|upper)|table\.(?:append|clear|clone|concat|create|extend|find|foreach|foreachi|freeze|getn|insert|isfrozen|maxn|move|pack|remove|shrink|sort|unpack)|utf8\.(?:char|codepoint|codes|len|offset)|uuid\.(?:create)|vector\.(?:abs|angle|ceil|clamp|create|cross|dot|floor|lerp|magnitude|max|min|normalize|sign))\b + (?<![^.]\.|:)\b(bit32\.(?:arshift|band|bnot|bor|btest|bxor|byteswap|countlz|countrz|extract|lrotate|lshift|replace|rrotate|rshift|s32|smul)|buffer\.(?:copy|create|fill|fromstring|len|readbits|readf32|readf64|readi16|readi32|readi8|readstring|readu16|readu32|readu8|tostring|writebits|writef32|writef64|writei16|writei32|writei8|writestring|writeu16|writeu32|writeu8)|coroutine\.(?:close|create|isyieldable|resume|running|status|wrap|yield)|debug\.(?:info|traceback)|llbase64\.(?:decode|encode)|lljson\.(?:decode|encode|sldecode|slencode)|llprim\.(?:ParticleSystem|SetMedia)|math\.(?:abs|acos|asin|atan|atan2|ceil|clamp|cos|cosh|deg|exp|floor|fmod|frexp|isfinite|isinf|isnan|ldexp|lerp|log|log10|map|max|min|modf|noise|pow|rad|random|randomseed|round|sign|sin|sinh|sqrt|tan|tanh)|os\.(?:clock|date|difftime|time)|quaternion\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|rotation\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|string\.(?:byte|char|find|format|gmatch|gsub|len|lower|match|pack|packsize|rep|reverse|split|sub|unpack|upper)|table\.(?:append|clear|clone|concat|create|extend|find|foreach|foreachi|freeze|getn|insert|isfrozen|maxn|move|pack|remove|shrink|sort|unpack)|utf8\.(?:char|codepoint|codes|len|offset)|uuid\.(?:create)|vector\.(?:abs|angle|ceil|clamp|create|cross|dot|floor|lerp|magnitude|max|min|normalize|sign))\b name @@ -1278,7 +1278,7 @@ match - \b(nil|any|boolean|buffer|never|number|string|thread|unknown|quaternion|uuid|vector|DetectedEvent|rotation|list|DateTypeArg|DateTypeResult)\b + \b(nil|any|boolean|buffer|never|number|string|thread|unknown|quaternion|uuid|vector|DetectedEvent|rotation|list|DateTypeArg|DateTypeResult|ParticleParams|MediaParams)\b name support.type.primitive.luau diff --git a/generated/syntax/slua.tmLanguage.json b/generated/syntax/slua.tmLanguage.json index b8369395..392d4430 100644 --- a/generated/syntax/slua.tmLanguage.json +++ b/generated/syntax/slua.tmLanguage.json @@ -362,7 +362,7 @@ }, { "name": "support.function.luau", - "match": "(?- @@ -4078,17 +4085,20 @@ constants: Note: Viewer settings affect this flag. It works by default on most prims. type: integer value: 8 + value-type: boolean PRIM_MEDIA_HEIGHT_PIXELS: member-of: [PrimMediaParam] tooltip: Integer. Gets or sets the height of the media in pixels. type: integer value: 10 + value-type: integer PRIM_MEDIA_HOME_URL: member-of: [PrimMediaParam] tooltip: String. Gets or sets the home URL for the chosen face (maximum 1024 characters). type: integer value: 3 + value-type: string PRIM_MEDIA_MAX_HEIGHT_PIXELS: tooltip: '' type: integer @@ -4119,12 +4129,14 @@ constants: media control bar (PRIM_MEDIA_PERM_ANYONE, _GROUP, _NONE, or _OWNER). type: integer value: 14 + value-type: integer PRIM_MEDIA_PERMS_INTERACT: member-of: [PrimMediaParam] tooltip: Integer. Gets or sets the permissions mask controlling who can interact with the media (PRIM_MEDIA_PERM_ANYONE, _GROUP, _NONE, or _OWNER). type: integer value: 13 + value-type: integer PRIM_MEDIA_PERM_ANYONE: member-of: [PrimMediaPerm] tooltip: '' @@ -4152,17 +4164,20 @@ constants: comes first). type: integer value: 12 + value-type: string PRIM_MEDIA_WHITELIST_ENABLE: member-of: [PrimMediaParam] tooltip: Boolean. Gets or sets whether media navigation is restricted to the URLs listed in PRIM_MEDIA_WHITELIST. type: integer value: 11 + value-type: boolean PRIM_MEDIA_WIDTH_PIXELS: member-of: [PrimMediaParam] tooltip: Integer. Gets or sets the width of the media in pixels. type: integer value: 9 + value-type: integer PRIM_NAME: member-of: [PrimParam] tooltip: Prim parameter [PRIM_NAME, string name] to get or set the prim's name @@ -16992,6 +17007,14 @@ builder-rulesets: flag-field: flags flag-mask: _MASK lua-module: llprim - lua-fn: ParticleSystem + lua-fn: particleSystem lua-type: ParticleParams dispatch-fn: LinkParticleSystem + + prim-media-params: + type: table + enum: PrimMediaParam + lua-module: llprim + lua-fn: setMedia + lua-type: MediaParams + dispatch-fn: SetLinkMedia diff --git a/lsl_definitions/slua.py b/lsl_definitions/slua.py index 7bd4a764..9c1e6259 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -21,6 +21,7 @@ _TABLE_RULESET_TYPE_MAP: dict[str, str] = { "float": "number", "integer": "number", + "boolean": "boolean", "vector": "vector", "string": "string", "key": "(string | uuid)", @@ -667,7 +668,7 @@ def make_fluent_method(builder_spec: BuilderSpec, method: BuilderMethod) -> SLua builder_class = [m for m in self.classes if m.name == spec.class_name][0] builder_class.methods.extend(methods) - def _generate_fluent_builder_classes(self, lsl: "LSLDefinitions") -> None: + def _generate_fluent_builder_classes(self, lsl: LSLDefinitions) -> None: """Inject typed properties into fluent builder classes or type aliases. For each table-type builder ruleset that names a lua-type, finds the matching diff --git a/slua_definitions.yaml b/slua_definitions.yaml index 2e85f773..71d0bf44 100644 --- a/slua_definitions.yaml +++ b/slua_definitions.yaml @@ -242,6 +242,11 @@ type-aliases: export: true definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("particle-params") selene-type: table +- name: MediaParams + comment: Prim face media parameter table. Pass to llprim.SetMedia() to configure media on a face. + export: true + definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("prim-media-params") + selene-type: table classes: - name: LLEvents comment: Event registration and management class for Second Life events @@ -1458,7 +1463,7 @@ modules: type: PrimParamsSetterTypeMeta value: PrimParamsSetterTypeMeta functions: - - name: ParticleSystem + - name: particleSystem comment: Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. parameters: - name: params @@ -1466,6 +1471,16 @@ modules: - name: link type: number? return-type: '()' + - name: setMedia + comment: Configure media on a specific face. Omit link to apply to the current prim. + parameters: + - name: face + type: number + - name: params + type: MediaParams? + - name: link + type: number? + return-type: number - name: math comment: Mathematical functions library. functions: From ee92d333e472efb6e8df7e32bc94ef06392dd581 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 22 Jun 2026 20:24:23 +0000 Subject: [PATCH 05/12] update generated files --- generated/cpp/fluent_builder_descriptors.cpp | 6 +++--- generated/lsl_keywords_pretty.xml | 4 ++-- generated/lua_keywords_pretty.xml | 8 ++++---- generated/secondlife.d.luau | 6 +++--- generated/secondlife.docs.json | 8 ++++---- generated/secondlife_selene.yml | 17 ++++++++++------- generated/syntax/slua.tmLanguage | 2 +- generated/syntax/slua.tmLanguage.json | 2 +- generated/templated/LslLibrary.cs | 6 +++--- 9 files changed, 31 insertions(+), 28 deletions(-) diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp index 1e4a5979..06663499 100644 --- a/generated/cpp/fluent_builder_descriptors.cpp +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -57,7 +57,7 @@ auto particle_system = [](lua_State* L) -> int { lua_call(L, 2, 0); return 0; }; -slua_register_fluent_fn(L, "llprim", "ParticleSystem", particle_system, kParticleParamsDef); +slua_register_fluent_fn(L, "llprim", "particleSystem", particle_system, kParticleParamsDef); // prim-media-params static const FluentParamDescriptor kPrimMediaParamsDescs[] = { @@ -73,7 +73,7 @@ static const FluentParamDescriptor kPrimMediaParamsDescs[] = { {"width_pixels", 'i', 9}, {"height_pixels", 'i', 10}, {"whitelist_enable", 'b', 11}, - {"whitelist", 's', 12}, + {"whitelist", 'C', 12}, {"perms_interact", 'i', 13}, {"perms_control", 'i', 14}, }; @@ -92,4 +92,4 @@ auto set_media = [](lua_State* L) -> int { lua_call(L, 3, 0); return 0; }; -slua_register_fluent_fn(L, "llprim", "SetMedia", set_media, kPrimMediaParamsDef); \ No newline at end of file +slua_register_fluent_fn(L, "llprim", "setMedia", set_media, kPrimMediaParamsDef); \ No newline at end of file diff --git a/generated/lsl_keywords_pretty.xml b/generated/lsl_keywords_pretty.xml index 2f1fbbc4..d2c10a18 100644 --- a/generated/lsl_keywords_pretty.xml +++ b/generated/lsl_keywords_pretty.xml @@ -2470,7 +2470,7 @@ This file is auto-generated by https://github.com/secondlife/lsl-definitions. -- HTTP_CUSTOM_HEADER tooltip - Adds a custom HTTP header (name and value pair) to the request. Up to 8 custom headers can be configured per request. Some default or sensitive headers are blocked for security reasons. + Adds custom HTTP headers to the request (up to 8 per request). At the SLua level, provide a string-keyed table mapping header names to values; the serializer emits one HTTP_CUSTOM_HEADER/key/value triple per entry. Some default or sensitive headers are blocked for security reasons. type integer value @@ -5643,7 +5643,7 @@ vector force PRIM_MEDIA_WHITELIST tooltip - String. Gets or sets the whitelist as a string of escaped, comma-separated URLs (supports up to 64 URLs or 1024 characters, whichever comes first). + Gets or sets the media whitelist as an array of URL strings (up to 64 URLs or 1024 characters, whichever comes first). At the SLua level, provide a {string} array; the serializer encodes it as an escaped, comma-separated string for the underlying LSL API. type integer value diff --git a/generated/lua_keywords_pretty.xml b/generated/lua_keywords_pretty.xml index a8f590e2..d239eff5 100644 --- a/generated/lua_keywords_pretty.xml +++ b/generated/lua_keywords_pretty.xml @@ -2733,7 +2733,7 @@ export type rotation = quaternion HTTP_CUSTOM_HEADER tooltip - Adds a custom HTTP header (name and value pair) to the request. Up to 8 custom headers can be configured per request. Some default or sensitive headers are blocked for security reasons. + Adds custom HTTP headers to the request (up to 8 per request). At the SLua level, provide a string-keyed table mapping header names to values; the serializer emits one HTTP_CUSTOM_HEADER/key/value triple per entry. Some default or sensitive headers are blocked for security reasons. type number value @@ -5926,7 +5926,7 @@ vector force PRIM_MEDIA_WHITELIST tooltip - String. Gets or sets the whitelist as a string of escaped, comma-separated URLs (supports up to 64 URLs or 1024 characters, whichever comes first). + Gets or sets the media whitelist as an array of URL strings (up to 64 URLs or 1024 characters, whichever comes first). At the SLua level, provide a {string} array; the serializer encodes it as an escaped, comma-separated string for the underlying LSL API. type number value @@ -13369,7 +13369,7 @@ Returns true if result is non-zero. tooltip Utilities for working with prims / objects - llprim.ParticleSystem + llprim.particleSystem arguments @@ -13401,7 +13401,7 @@ Returns true if result is non-zero. tooltip Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. - llprim.SetMedia + llprim.setMedia arguments diff --git a/generated/secondlife.d.luau b/generated/secondlife.d.luau index b712839c..138326d3 100644 --- a/generated/secondlife.d.luau +++ b/generated/secondlife.d.luau @@ -137,7 +137,7 @@ export type MediaParams = { width_pixels: number?, height_pixels: number?, whitelist_enable: boolean?, - whitelist: string?, + whitelist: {string}?, perms_interact: number?, perms_control: number?, } @@ -441,8 +441,8 @@ declare lljson: { declare llprim: { ParamsSetter: PrimParamsSetterTypeMeta, - ParticleSystem: (params: ParticleParams?, link: number?) -> (), - SetMedia: (face: number, params: MediaParams?, link: number?) -> number, + particleSystem: (params: ParticleParams?, link: number?) -> (), + setMedia: (face: number, params: MediaParams?, link: number?) -> number, } diff --git a/generated/secondlife.docs.json b/generated/secondlife.docs.json index 84ec0fa2..38ee8dc8 100644 --- a/generated/secondlife.docs.json +++ b/generated/secondlife.docs.json @@ -370,10 +370,10 @@ "@sl-slua/global/llprim.ParamsSetter": { "documentation": "Value: PrimParamsSetterTypeMeta
Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast" }, - "@sl-slua/global/llprim.ParticleSystem": { + "@sl-slua/global/llprim.particleSystem": { "documentation": "Emit a particle system from a ParticleParams table. Omit link to apply to the current prim." }, - "@sl-slua/global/llprim.SetMedia": { + "@sl-slua/global/llprim.setMedia": { "documentation": "Configure media on a specific face. Omit link to apply to the current prim." }, "@sl-slua/global/math": { @@ -5823,7 +5823,7 @@ "documentation": "Value: 0
Indicates the response body truncation point in bytes." }, "@sl-slua/global/HTTP_CUSTOM_HEADER": { - "documentation": "Value: 5
Adds a custom HTTP header (name and value pair) to the request. Up to 8 custom headers can be configured per request. Some default or sensitive headers are blocked for security reasons." + "documentation": "Value: 5
Adds custom HTTP headers to the request (up to 8 per request). At the SLua level, provide a string-keyed table mapping header names to values; the serializer emits one HTTP_CUSTOM_HEADER/key/value triple per entry. Some default or sensitive headers are blocked for security reasons." }, "@sl-slua/global/HTTP_EXTENDED_ERROR": { "documentation": "Value: 9
If set to TRUE, llHTTPRequest always returns a key. If an error occurs during the request, detailed error information (delivered in an RFC 7807 JSON block) is reported through the http_response event using that key." @@ -6870,7 +6870,7 @@ "documentation": "Value: 1" }, "@sl-slua/global/PRIM_MEDIA_WHITELIST": { - "documentation": "Value: 12
String. Gets or sets the whitelist as a string of escaped, comma-separated URLs (supports up to 64 URLs or 1024 characters, whichever comes first)." + "documentation": "Value: 12
Gets or sets the media whitelist as an array of URL strings (up to 64 URLs or 1024 characters, whichever comes first). At the SLua level, provide a {string} array; the serializer encodes it as an escaped, comma-separated string for the underlying LSL API." }, "@sl-slua/global/PRIM_MEDIA_WHITELIST_ENABLE": { "documentation": "Value: 11
Boolean. Gets or sets whether media navigation is restricted to the URLs listed in PRIM_MEDIA_WHITELIST." diff --git a/generated/secondlife_selene.yml b/generated/secondlife_selene.yml index 33693292..fd31387a 100644 --- a/generated/secondlife_selene.yml +++ b/generated/secondlife_selene.yml @@ -1179,9 +1179,10 @@ globals: HTTP_CUSTOM_HEADER: property: read-only type: number - description: Adds a custom HTTP header (name and value pair) to the request. Up - to 8 custom headers can be configured per request. Some default or sensitive - headers are blocked for security reasons. + description: Adds custom HTTP headers to the request (up to 8 per request). At + the SLua level, provide a string-keyed table mapping header names to values; + the serializer emits one HTTP_CUSTOM_HEADER/key/value triple per entry. Some + default or sensitive headers are blocked for security reasons. HTTP_EXTENDED_ERROR: property: read-only type: number @@ -2886,8 +2887,10 @@ globals: PRIM_MEDIA_WHITELIST: property: read-only type: number - description: String. Gets or sets the whitelist as a string of escaped, comma-separated - URLs (supports up to 64 URLs or 1024 characters, whichever comes first). + description: Gets or sets the media whitelist as an array of URL strings (up to + 64 URLs or 1024 characters, whichever comes first). At the SLua level, provide + a {string} array; the serializer encodes it as an escaped, comma-separated string + for the underlying LSL API. PRIM_MEDIA_WHITELIST_ENABLE: property: read-only type: number @@ -5507,7 +5510,7 @@ globals: llprim.ParamsSetter: property: read-only description: Metatable for building lists to pass to ll.SetLinkPrimitiveParamsFast - llprim.ParticleSystem: + llprim.particleSystem: args: - type: table required: false @@ -5515,7 +5518,7 @@ globals: required: false description: Emit a particle system from a ParticleParams table. Omit link to apply to the current prim. - llprim.SetMedia: + llprim.setMedia: args: - type: number - type: table diff --git a/generated/syntax/slua.tmLanguage b/generated/syntax/slua.tmLanguage index 6ff66913..34a9de13 100644 --- a/generated/syntax/slua.tmLanguage +++ b/generated/syntax/slua.tmLanguage @@ -621,7 +621,7 @@ name support.function.luau match - (?<![^.]\.|:)\b(bit32\.(?:arshift|band|bnot|bor|btest|bxor|byteswap|countlz|countrz|extract|lrotate|lshift|replace|rrotate|rshift|s32|smul)|buffer\.(?:copy|create|fill|fromstring|len|readbits|readf32|readf64|readi16|readi32|readi8|readstring|readu16|readu32|readu8|tostring|writebits|writef32|writef64|writei16|writei32|writei8|writestring|writeu16|writeu32|writeu8)|coroutine\.(?:close|create|isyieldable|resume|running|status|wrap|yield)|debug\.(?:info|traceback)|llbase64\.(?:decode|encode)|lljson\.(?:decode|encode|sldecode|slencode)|llprim\.(?:ParticleSystem|SetMedia)|math\.(?:abs|acos|asin|atan|atan2|ceil|clamp|cos|cosh|deg|exp|floor|fmod|frexp|isfinite|isinf|isnan|ldexp|lerp|log|log10|map|max|min|modf|noise|pow|rad|random|randomseed|round|sign|sin|sinh|sqrt|tan|tanh)|os\.(?:clock|date|difftime|time)|quaternion\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|rotation\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|string\.(?:byte|char|find|format|gmatch|gsub|len|lower|match|pack|packsize|rep|reverse|split|sub|unpack|upper)|table\.(?:append|clear|clone|concat|create|extend|find|foreach|foreachi|freeze|getn|insert|isfrozen|maxn|move|pack|remove|shrink|sort|unpack)|utf8\.(?:char|codepoint|codes|len|offset)|uuid\.(?:create)|vector\.(?:abs|angle|ceil|clamp|create|cross|dot|floor|lerp|magnitude|max|min|normalize|sign))\b + (?<![^.]\.|:)\b(bit32\.(?:arshift|band|bnot|bor|btest|bxor|byteswap|countlz|countrz|extract|lrotate|lshift|replace|rrotate|rshift|s32|smul)|buffer\.(?:copy|create|fill|fromstring|len|readbits|readf32|readf64|readi16|readi32|readi8|readstring|readu16|readu32|readu8|tostring|writebits|writef32|writef64|writei16|writei32|writei8|writestring|writeu16|writeu32|writeu8)|coroutine\.(?:close|create|isyieldable|resume|running|status|wrap|yield)|debug\.(?:info|traceback)|llbase64\.(?:decode|encode)|lljson\.(?:decode|encode|sldecode|slencode)|llprim\.(?:particleSystem|setMedia)|math\.(?:abs|acos|asin|atan|atan2|ceil|clamp|cos|cosh|deg|exp|floor|fmod|frexp|isfinite|isinf|isnan|ldexp|lerp|log|log10|map|max|min|modf|noise|pow|rad|random|randomseed|round|sign|sin|sinh|sqrt|tan|tanh)|os\.(?:clock|date|difftime|time)|quaternion\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|rotation\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|string\.(?:byte|char|find|format|gmatch|gsub|len|lower|match|pack|packsize|rep|reverse|split|sub|unpack|upper)|table\.(?:append|clear|clone|concat|create|extend|find|foreach|foreachi|freeze|getn|insert|isfrozen|maxn|move|pack|remove|shrink|sort|unpack)|utf8\.(?:char|codepoint|codes|len|offset)|uuid\.(?:create)|vector\.(?:abs|angle|ceil|clamp|create|cross|dot|floor|lerp|magnitude|max|min|normalize|sign))\b name diff --git a/generated/syntax/slua.tmLanguage.json b/generated/syntax/slua.tmLanguage.json index 392d4430..cedaef2c 100644 --- a/generated/syntax/slua.tmLanguage.json +++ b/generated/syntax/slua.tmLanguage.json @@ -362,7 +362,7 @@ }, { "name": "support.function.luau", - "match": "(? Date: Mon, 22 Jun 2026 20:34:56 +0000 Subject: [PATCH 06/12] Adding llSetMediaParams with white list exposed as array of strings. --- generated/cpp/fluent_builder_descriptors.cpp | 4 +- lsl_definitions.schema.json | 4 +- lsl_definitions.yaml | 17 ++- lsl_definitions/generators/lscript.py | 133 +++++++++++++++---- lsl_definitions/rulesets.py | 30 ++++- lsl_definitions/slua.py | 2 + 6 files changed, 148 insertions(+), 42 deletions(-) diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp index 06663499..1a4777c2 100644 --- a/generated/cpp/fluent_builder_descriptors.cpp +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -89,7 +89,7 @@ auto set_media = [](lua_State* L) -> int { lua_pushinteger(L, link); lua_pushinteger(L, face); lua_pushvalue(L, rules_idx); - lua_call(L, 3, 0); - return 0; + lua_call(L, 3, 1); + return 1; }; slua_register_fluent_fn(L, "llprim", "setMedia", set_media, kPrimMediaParamsDef); \ No newline at end of file diff --git a/lsl_definitions.schema.json b/lsl_definitions.schema.json index dcde0d36..c3652f47 100644 --- a/lsl_definitions.schema.json +++ b/lsl_definitions.schema.json @@ -355,8 +355,8 @@ "items": { "type": "string", "pattern": "^[A-Z]+[A-Za-z_]*$" } }, "value-type": { - "markdownDescription": "For constants used as rule tags in list-based APIs: the type of the value that follows this constant in the list. Uses the same vocabulary as ruleset param types.", - "enum": ["integer", "float", "string", "vector", "rotation", "boolean", "asset", "key"] + "markdownDescription": "For constants used as rule tags in list-based APIs: the type of the value that follows this constant in the list. Uses the same vocabulary as ruleset param types. 'string-csv' accepts a {string} array and serializes it as an escaped comma-separated string. 'string-map' accepts a {[string]: string} table and emits one tag/key/value triple per entry.", + "enum": ["integer", "float", "string", "vector", "rotation", "boolean", "asset", "key", "string-csv", "string-map"] }, "pretty-name": { "markdownDescription": "Override the auto-generated property alias for this constant when used as a table-ruleset key. Must be a valid Lua identifier (lowercase, underscores).", diff --git a/lsl_definitions.yaml b/lsl_definitions.yaml index e5d618f2..2de4ad8a 100644 --- a/lsl_definitions.yaml +++ b/lsl_definitions.yaml @@ -1978,11 +1978,13 @@ constants: value: 0 HTTP_CUSTOM_HEADER: member-of: [HTTPRequestParam] - tooltip: Adds a custom HTTP header (name and value pair) to the request. Up to 8 - custom headers can be configured per request. Some default or sensitive - headers are blocked for security reasons. + tooltip: Adds custom HTTP headers to the request (up to 8 per request). At + the SLua level, provide a string-keyed table mapping header names to + values; the serializer emits one HTTP_CUSTOM_HEADER/key/value triple per + entry. Some default or sensitive headers are blocked for security reasons. type: integer value: 5 + value-type: string-map HTTP_EXTENDED_ERROR: member-of: [HTTPRequestParam] tooltip: If set to TRUE, llHTTPRequest always returns a key. If an error occurs @@ -4159,12 +4161,13 @@ constants: value: 1 PRIM_MEDIA_WHITELIST: member-of: [PrimMediaParam] - tooltip: String. Gets or sets the whitelist as a string of escaped, - comma-separated URLs (supports up to 64 URLs or 1024 characters, whichever - comes first). + tooltip: Gets or sets the media whitelist as an array of URL strings (up to + 64 URLs or 1024 characters, whichever comes first). At the SLua level, + provide a {string} array; the serializer encodes it as an escaped, + comma-separated string for the underlying LSL API. type: integer value: 12 - value-type: string + value-type: string-csv PRIM_MEDIA_WHITELIST_ENABLE: member-of: [PrimMediaParam] tooltip: Boolean. Gets or sets whether media navigation is restricted to the diff --git a/lsl_definitions/generators/lscript.py b/lsl_definitions/generators/lscript.py index d732ec70..c4ac9163 100644 --- a/lsl_definitions/generators/lscript.py +++ b/lsl_definitions/generators/lscript.py @@ -7,7 +7,7 @@ from lsl_definitions.generators.base import register from lsl_definitions.lsl import LSLDefinitions, LSLFunction, LSLType -from lsl_definitions.rulesets import expand_table_ruleset +from lsl_definitions.rulesets import _VALUE_TYPE_TO_SEMANTIC, expand_table_ruleset from lsl_definitions.utils import is_uuid, splice_str, to_c_str @@ -47,6 +47,93 @@ def gen_lscript_library_defs(definitions: LSLDefinitions) -> str: # since we don't really know if they did anything weird in their implementation or definition (maybe differing # energy / sleep values.) Better to leave them alone and only generate new functions. # For testing, we omit from this blacklist `llAxisAngle2Rot` and `llListSortStrided`, and manually verify the output. +# C# reserved keywords that must be escaped with @ when used as identifiers +_CS_RESERVED_KEYWORDS = { + "abstract", + "as", + "base", + "bool", + "break", + "byte", + "case", + "catch", + "char", + "checked", + "class", + "const", + "continue", + "decimal", + "default", + "delegate", + "do", + "double", + "else", + "enum", + "event", + "explicit", + "extern", + "false", + "finally", + "fixed", + "float", + "for", + "foreach", + "goto", + "if", + "implicit", + "in", + "int", + "interface", + "internal", + "is", + "lock", + "long", + "namespace", + "new", + "null", + "object", + "operator", + "out", + "override", + "params", + "private", + "protected", + "public", + "readonly", + "ref", + "return", + "sbyte", + "sealed", + "short", + "sizeof", + "stackalloc", + "static", + "string", + "struct", + "switch", + "this", + "throw", + "true", + "try", + "typeof", + "uint", + "ulong", + "unchecked", + "unsafe", + "ushort", + "using", + "virtual", + "void", + "volatile", + "while", +} + + +def _cs_ident(name: str) -> str: + """Escape a C# reserved keyword by prefixing with @.""" + return f"@{name}" if name in _CS_RESERVED_KEYWORDS else name + + _OLD_FUNC_BLACKLIST = { "llGiveInventory", "llModPow", @@ -597,10 +684,10 @@ def gen_mono_library_defs(definitions: LSLDefinitions, template_path: str) -> st args_strs = [] for arg in func.arguments: if arg.type == LSLType.LIST: - args_strs.append(f"object[] {arg.name}") + args_strs.append(f"object[] {_cs_ident(arg.name)}") args_strs.append(f"int {arg.name}_len") else: - args_strs.append(f"{arg.type.meta.cs_name} {arg.name}") + args_strs.append(f"{arg.type.meta.cs_name} {_cs_ident(arg.name)}") args_str = ", ".join(args_strs) new_defs += ( @@ -608,7 +695,9 @@ def gen_mono_library_defs(definitions: LSLDefinitions, template_path: str) -> st ) # Now for the wrapper implementation - args_str = ", ".join(f"{a.type.meta.cs_name} {a.name}" for a in func.arguments) + args_str = ", ".join( + f"{a.type.meta.cs_name} {_cs_ident(a.name)}" for a in func.arguments + ) new_defs += ( f" public static {func.ret_type.meta.cs_name} {func.name}({args_str}) {{\n" ) @@ -616,10 +705,10 @@ def gen_mono_library_defs(definitions: LSLDefinitions, template_path: str) -> st call_args = [] for arg in func.arguments: if arg.type == LSLType.LIST: - call_args.append(f"ToArrayNoCopy({arg.name})") - call_args.append(f"{arg.name}.Count") + call_args.append(f"ToArrayNoCopy({_cs_ident(arg.name)})") + call_args.append(f"{_cs_ident(arg.name)}.Count") else: - call_args.append(arg.name) + call_args.append(_cs_ident(arg.name)) call_expr = f"{func.name}Internal({', '.join(call_args)})" if func.ret_type == LSLType.LIST: @@ -633,7 +722,9 @@ def gen_mono_library_defs(definitions: LSLDefinitions, template_path: str) -> st new_defs += f" return {call_expr};\n" new_defs += " }\n\n" else: - args_str = ", ".join(f"{a.type.meta.cs_name} {a.name}" for a in func.arguments) + args_str = ", ".join( + f"{a.type.meta.cs_name} {_cs_ident(a.name)}" for a in func.arguments + ) new_defs += f" public extern static {func.ret_type.meta.cs_name} {func.name}({args_str});\n" return splice_str(template, _MONO_CS_LIBRARY_DEFS_COMMENT, new_defs) @@ -897,16 +988,7 @@ def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: - a slua_register_fluent_fn call The resulting fragment is #include'd inside init_fluent_builders(lua_State* L). """ - _semantic_map = { - "integer": "i", - "float": "f", - "string": "s", - "vector": "v", - "rotation": "r", - "boolean": "b", - "asset": "a", - "key": "k", - } + _semantic_map = _VALUE_TYPE_TO_SEMANTIC def _inspect_dispatch_fn(dispatch_fn_name: str): """Inspect ll{dispatch_fn_name}'s argument list. @@ -940,7 +1022,7 @@ def _inspect_dispatch_fn(dispatch_fn_name: str): if is_link: has_link = True result.append((arg.name, is_link)) - return result, has_link + return result, has_link, func.ret_type def _lua_fn_to_cpp_name(lua_fn: str) -> str: """Convert camelCase Lua function name to snake_case C++ variable name. @@ -948,7 +1030,9 @@ def _lua_fn_to_cpp_name(lua_fn: str) -> str: """ return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", lua_fn).lower() - def _build_wrapper(cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: bool) -> str: + def _build_wrapper( + cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: bool, ret_type: LSLType + ) -> str: """Emit a C++ lambda that reads Lua args, serializes params, and calls ll.dispatch_fn. Lua stack layout: @@ -991,8 +1075,9 @@ def _build_wrapper(cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: else: lines.append(f" lua_pushinteger(L, {arg_name.lower()});") lines.append(" lua_pushvalue(L, rules_idx);") - lines.append(f" lua_call(L, {total_ll_args}, 0);") - lines.append(" return 0;") + nresults = 0 if ret_type == LSLType.VOID else 1 + lines.append(f" lua_call(L, {total_ll_args}, {nresults});") + lines.append(f" return {nresults};") body = "\n".join(lines) return ( @@ -1012,7 +1097,7 @@ def _build_wrapper(cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: lua_fn = ruleset_data["lua-fn"] dispatch_fn = ruleset_data["dispatch-fn"] - prefix_args, has_link = _inspect_dispatch_fn(dispatch_fn) + prefix_args, has_link, ret_type = _inspect_dispatch_fn(dispatch_fn) cpp_name = _lua_fn_to_cpp_name(lua_fn) # Descriptor array/def names derived from the enum name. @@ -1097,7 +1182,7 @@ def _build_wrapper(cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: f"fluent_builder_def_build({array_name}, std::size({array_name}));\n" ) - section += _build_wrapper(cpp_name, dispatch_fn, prefix_args, has_link) + "\n" + section += _build_wrapper(cpp_name, dispatch_fn, prefix_args, has_link, ret_type) + "\n" section += ( f'slua_register_fluent_fn(L, "{lua_module}", "{lua_fn}", {cpp_name}, {def_name});' ) diff --git a/lsl_definitions/rulesets.py b/lsl_definitions/rulesets.py index 59ceebd1..a0956e10 100644 --- a/lsl_definitions/rulesets.py +++ b/lsl_definitions/rulesets.py @@ -7,7 +7,7 @@ from __future__ import annotations import dataclasses -from typing import TYPE_CHECKING, List, Optional, Set +from typing import TYPE_CHECKING if TYPE_CHECKING: from lsl_definitions.lsl import LSLDefinitions @@ -35,6 +35,22 @@ class BuilderParamType: ] } +# Maps value-type annotation string → FluentParamDescriptor semantic char. +# Consumed by any code that builds a C array of FluentParamDescriptor from +# a MemberDescriptor list. +_VALUE_TYPE_TO_SEMANTIC: dict[str, str] = { + "integer": "i", + "float": "f", + "string": "s", + "vector": "v", + "rotation": "r", + "boolean": "b", + "asset": "a", + "key": "k", + "string-csv": "C", + "string-map": "M", +} + @dataclasses.dataclass(frozen=True) class BuilderMethodParam: @@ -70,7 +86,7 @@ class MemberDescriptor: strict_name: str """Always-valid property name (prefix stripped, lowercased).""" - pretty_name: Optional[str] + pretty_name: str | None """Pretty alias, or None if reverted due to collision.""" tag: int """Numeric value of the originating constant (the rule tag).""" @@ -150,8 +166,8 @@ def expand_spp_builder(lsl: LSLDefinitions) -> BuilderSpec: def expand_member_params( lsl: LSLDefinitions, enum_name: str, - filler_tokens: Set[str], -) -> List[MemberDescriptor]: + filler_tokens: set[str], +) -> list[MemberDescriptor]: """Derive a sorted, collision-resolved list of property descriptors from the constants that are members of *enum_name*. @@ -170,7 +186,7 @@ def expand_member_params( key=lambda c: int(c.value, 0), ) - rows: List[tuple] = [] + rows: list[tuple] = [] for const in candidates: name = const.name prefix = enum.prefix @@ -190,7 +206,7 @@ def expand_member_params( # Pass 2: pretty aliases. # A constant's pretty_name field takes priority over the filler-token derivation. - def _pretty(strict: str, override: Optional[str]) -> str: + def _pretty(strict: str, override: str | None) -> str: if override is not None: return override tokens = [t for t in strict.split("_") if t not in filler_tokens] @@ -227,7 +243,7 @@ def _pretty(strict: str, override: Optional[str]) -> str: ] -def expand_table_ruleset(lsl: LSLDefinitions, ruleset_name: str) -> List[MemberDescriptor]: +def expand_table_ruleset(lsl: LSLDefinitions, ruleset_name: str) -> list[MemberDescriptor]: """Expand a type:table builder-ruleset into a sorted property descriptor list. Reads ``enum`` and ``filler-tokens`` from the yaml entry. diff --git a/lsl_definitions/slua.py b/lsl_definitions/slua.py index 9c1e6259..bbaf730a 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -26,6 +26,8 @@ "string": "string", "key": "(string | uuid)", "asset": "(string | uuid)", + "string-csv": "{string}", + "string-map": "{[string]: string}", } if TYPE_CHECKING: From eb15da1bac90d27fdf42e51a0f6b5b6424c48058 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 23 Jun 2026 16:05:17 +0000 Subject: [PATCH 07/12] Update generated files --- generated/cpp/fluent_builder_descriptors.cpp | 28 +++++++++++ generated/lua_keywords_pretty.xml | 53 ++++++++++++++++++++ generated/secondlife.d.luau | 21 ++++++++ generated/secondlife.docs.json | 6 +++ generated/secondlife_selene.yml | 10 ++++ generated/syntax/slua.tmLanguage | 6 +-- generated/syntax/slua.tmLanguage.json | 6 +-- 7 files changed, 124 insertions(+), 6 deletions(-) diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp index 1a4777c2..e54b651d 100644 --- a/generated/cpp/fluent_builder_descriptors.cpp +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -1,3 +1,31 @@ +// http-params +static const FluentParamDescriptor kHTTPRequestParamsDescs[] = { + {"method", 's', 0}, + {"mimetype", 's', 1}, + {"body_maxlength", 'i', 2}, + {"verify_cert", 'b', 3}, + {"verbose_throttle", 'b', 4}, + {"custom_header", 'M', 5}, + {"pragma_no_cache", 'b', 6}, + {"user_agent", 's', 7}, + {"accept", 'N', 8}, + {"extended_error", 'b', 9}, +}; +static FluentBuilderDef* kHTTPRequestParamsDef = fluent_builder_def_build(kHTTPRequestParamsDescs, std::size(kHTTPRequestParamsDescs)); +auto request = [](lua_State* L) -> int { + const auto* def = (const FluentBuilderDef*)lua_tolightuserdata(L, lua_upvalueindex(1)); + slua_fluent_serialize(L, 2, def); + int rules_idx = lua_gettop(L); + lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "ll"); + lua_rawgetfield(L, -1, "HTTPRequest"); + lua_pushvalue(L, 1); + lua_pushvalue(L, rules_idx); + lua_pushvalue(L, 3); + lua_call(L, 3, 1); + return 1; +}; +slua_register_fluent_fn(L, "llhttp", "request", request, kHTTPRequestParamsDef); + // particle-params static const FluentParamDescriptor kParticleParamsDescs[] = { {"flags", 'i', 0}, diff --git a/generated/lua_keywords_pretty.xml b/generated/lua_keywords_pretty.xml index d239eff5..c97f3bc6 100644 --- a/generated/lua_keywords_pretty.xml +++ b/generated/lua_keywords_pretty.xml @@ -198,6 +198,11 @@ export type DateTypeResult = { isdst: boolean?, }
+ HttpRequestParams + + tooltip + HTTP request parameter table. Pass to ll.httpRequest() to configure an HTTP request. + MediaParams tooltip @@ -13227,6 +13232,54 @@ Returns true if result is non-zero. tooltip Encodes a string or buffer to base64 + llhttp + + energy + -1.0 + tooltip + Utilities for making HTTP requests. + + llhttp.request + + arguments + + + url + + tooltip + + type + string + + + + params + + tooltip + + type + HttpRequestParams? + + + + body + + tooltip + + type + string? + + + + energy + 10.0 + return + uuid + sleep + 0.0 + tooltip + Send an HTTP request. Returns a key identifying the request, which is delivered via the http_response event. Omit body for requests that do not need a body (e.g. GET). + lljson energy diff --git a/generated/secondlife.d.luau b/generated/secondlife.d.luau index 138326d3..8aaa19df 100644 --- a/generated/secondlife.d.luau +++ b/generated/secondlife.d.luau @@ -141,6 +141,18 @@ export type MediaParams = { perms_interact: number?, perms_control: number?, } +export type HttpRequestParams = { + method: string?, + mimetype: string?, + body_maxlength: number?, + verify_cert: boolean?, + verbose_throttle: boolean?, + custom_header: {[string]: string | number | boolean | vector | quaternion}?, + pragma_no_cache: boolean?, + user_agent: string?, + accept: {string}?, + extended_error: boolean?, +} declare extern type DetectedEvent with index: number @@ -417,6 +429,15 @@ declare llbase64: { } +--------------------------- +-- Global Table: llhttp +--------------------------- + +declare llhttp: { + request: (url: string, params: HttpRequestParams?, body: string?) -> uuid, +} + + --------------------------- -- Global Table: lljson --------------------------- diff --git a/generated/secondlife.docs.json b/generated/secondlife.docs.json index 38ee8dc8..1a254222 100644 --- a/generated/secondlife.docs.json +++ b/generated/secondlife.docs.json @@ -320,6 +320,12 @@ "@sl-slua/global/llbase64.decode": { "documentation": "Decodes a base64 string to a string, or buffer if asBuffer is true. The output is truncated at the first decoding error." }, + "@sl-slua/global/llhttp": { + "documentation": "Utilities for making HTTP requests." + }, + "@sl-slua/global/llhttp.request": { + "documentation": "Send an HTTP request. Returns a key identifying the request, which is delivered via the http_response event. Omit body for requests that do not need a body (e.g. GET)." + }, "@sl-slua/global/lljson": { "documentation": "JSON encoding/decoding library for Second Life.", "learn_more_link": "https://create.secondlife.com/script/learn-slua/json/" diff --git a/generated/secondlife_selene.yml b/generated/secondlife_selene.yml index fd31387a..2a18592c 100644 --- a/generated/secondlife_selene.yml +++ b/generated/secondlife_selene.yml @@ -5452,6 +5452,16 @@ globals: must_use: true description: Decodes a base64 string to a string, or buffer if asBuffer is true. The output is truncated at the first decoding error. + llhttp.request: + args: + - type: string + - type: table + required: false + - type: string + required: false + description: Send an HTTP request. Returns a key identifying the request, which + is delivered via the http_response event. Omit body for requests that do not + need a body (e.g. GET). lljson.null: property: read-only description: A constant to pass for null to json encode. diff --git a/generated/syntax/slua.tmLanguage b/generated/syntax/slua.tmLanguage index 34a9de13..d6913fa0 100644 --- a/generated/syntax/slua.tmLanguage +++ b/generated/syntax/slua.tmLanguage @@ -621,7 +621,7 @@ name support.function.luau match - (?<![^.]\.|:)\b(bit32\.(?:arshift|band|bnot|bor|btest|bxor|byteswap|countlz|countrz|extract|lrotate|lshift|replace|rrotate|rshift|s32|smul)|buffer\.(?:copy|create|fill|fromstring|len|readbits|readf32|readf64|readi16|readi32|readi8|readstring|readu16|readu32|readu8|tostring|writebits|writef32|writef64|writei16|writei32|writei8|writestring|writeu16|writeu32|writeu8)|coroutine\.(?:close|create|isyieldable|resume|running|status|wrap|yield)|debug\.(?:info|traceback)|llbase64\.(?:decode|encode)|lljson\.(?:decode|encode|sldecode|slencode)|llprim\.(?:particleSystem|setMedia)|math\.(?:abs|acos|asin|atan|atan2|ceil|clamp|cos|cosh|deg|exp|floor|fmod|frexp|isfinite|isinf|isnan|ldexp|lerp|log|log10|map|max|min|modf|noise|pow|rad|random|randomseed|round|sign|sin|sinh|sqrt|tan|tanh)|os\.(?:clock|date|difftime|time)|quaternion\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|rotation\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|string\.(?:byte|char|find|format|gmatch|gsub|len|lower|match|pack|packsize|rep|reverse|split|sub|unpack|upper)|table\.(?:append|clear|clone|concat|create|extend|find|foreach|foreachi|freeze|getn|insert|isfrozen|maxn|move|pack|remove|shrink|sort|unpack)|utf8\.(?:char|codepoint|codes|len|offset)|uuid\.(?:create)|vector\.(?:abs|angle|ceil|clamp|create|cross|dot|floor|lerp|magnitude|max|min|normalize|sign))\b + (?<![^.]\.|:)\b(bit32\.(?:arshift|band|bnot|bor|btest|bxor|byteswap|countlz|countrz|extract|lrotate|lshift|replace|rrotate|rshift|s32|smul)|buffer\.(?:copy|create|fill|fromstring|len|readbits|readf32|readf64|readi16|readi32|readi8|readstring|readu16|readu32|readu8|tostring|writebits|writef32|writef64|writei16|writei32|writei8|writestring|writeu16|writeu32|writeu8)|coroutine\.(?:close|create|isyieldable|resume|running|status|wrap|yield)|debug\.(?:info|traceback)|llbase64\.(?:decode|encode)|llhttp\.(?:request)|lljson\.(?:decode|encode|sldecode|slencode)|llprim\.(?:particleSystem|setMedia)|math\.(?:abs|acos|asin|atan|atan2|ceil|clamp|cos|cosh|deg|exp|floor|fmod|frexp|isfinite|isinf|isnan|ldexp|lerp|log|log10|map|max|min|modf|noise|pow|rad|random|randomseed|round|sign|sin|sinh|sqrt|tan|tanh)|os\.(?:clock|date|difftime|time)|quaternion\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|rotation\.(?:conjugate|create|dot|magnitude|normalize|slerp|tofwd|toleft|toup)|string\.(?:byte|char|find|format|gmatch|gsub|len|lower|match|pack|packsize|rep|reverse|split|sub|unpack|upper)|table\.(?:append|clear|clone|concat|create|extend|find|foreach|foreachi|freeze|getn|insert|isfrozen|maxn|move|pack|remove|shrink|sort|unpack)|utf8\.(?:char|codepoint|codes|len|offset)|uuid\.(?:create)|vector\.(?:abs|angle|ceil|clamp|create|cross|dot|floor|lerp|magnitude|max|min|normalize|sign))\b name @@ -645,7 +645,7 @@ name support.constant.luau match - (?<![^.]\.|:)\bbit32|buffer|coroutine|debug|ll|llbase64|llcompat|lljson(\.array_mt|empty_array|empty_object|null|object_mt|remove)?|llprim(\.ParamsSetter)?|math(\.huge|pi)?|os|quaternion(\.identity)?|rotation(\.identity)?|string|table|utf8(\.charpattern)?|uuid|vector(\.one|zero)?\b + (?<![^.]\.|:)\bbit32|buffer|coroutine|debug|ll|llbase64|llcompat|llhttp|lljson(\.array_mt|empty_array|empty_object|null|object_mt|remove)?|llprim(\.ParamsSetter)?|math(\.huge|pi)?|os|quaternion(\.identity)?|rotation(\.identity)?|string|table|utf8(\.charpattern)?|uuid|vector(\.one|zero)?\b name @@ -1278,7 +1278,7 @@ match - \b(nil|any|boolean|buffer|never|number|string|thread|unknown|quaternion|uuid|vector|DetectedEvent|rotation|list|DateTypeArg|DateTypeResult|ParticleParams|MediaParams)\b + \b(nil|any|boolean|buffer|never|number|string|thread|unknown|quaternion|uuid|vector|DetectedEvent|rotation|list|DateTypeArg|DateTypeResult|ParticleParams|MediaParams|HttpRequestParams)\b name support.type.primitive.luau diff --git a/generated/syntax/slua.tmLanguage.json b/generated/syntax/slua.tmLanguage.json index cedaef2c..6f3fbe89 100644 --- a/generated/syntax/slua.tmLanguage.json +++ b/generated/syntax/slua.tmLanguage.json @@ -362,7 +362,7 @@ }, { "name": "support.function.luau", - "match": "(? Date: Tue, 23 Jun 2026 16:11:35 +0000 Subject: [PATCH 08/12] Adding LLHTTPRequest with header and accept processing. --- lsl_definitions.schema.json | 8 +- lsl_definitions.yaml | 18 ++++ lsl_definitions/generators/lscript.py | 123 +++++++++++++++++--------- lsl_definitions/rulesets.py | 1 + lsl_definitions/slua.py | 3 +- slua_definitions.yaml | 20 +++++ 6 files changed, 126 insertions(+), 47 deletions(-) diff --git a/lsl_definitions.schema.json b/lsl_definitions.schema.json index c3652f47..bed19841 100644 --- a/lsl_definitions.schema.json +++ b/lsl_definitions.schema.json @@ -280,7 +280,11 @@ "required": ["type"] }, "then": { "required": ["rules"] }, - "else": { "required": ["lua-module", "lua-type", "lua-fn", "dispatch-fn"] } + "else": { + "required": ["enum", "lua-type"], + "if": { "required": ["dispatch-fn"] }, + "then": { "required": ["lua-module", "lua-fn"] } + } } }, "additionalProperties": false, @@ -356,7 +360,7 @@ }, "value-type": { "markdownDescription": "For constants used as rule tags in list-based APIs: the type of the value that follows this constant in the list. Uses the same vocabulary as ruleset param types. 'string-csv' accepts a {string} array and serializes it as an escaped comma-separated string. 'string-map' accepts a {[string]: string} table and emits one tag/key/value triple per entry.", - "enum": ["integer", "float", "string", "vector", "rotation", "boolean", "asset", "key", "string-csv", "string-map"] + "enum": ["integer", "float", "string", "vector", "rotation", "boolean", "asset", "key", "string-csv", "string-map", "string-multi"] }, "pretty-name": { "markdownDescription": "Override the auto-generated property alias for this constant when used as a table-ruleset key. Must be a valid Lua identifier (lowercase, underscores).", diff --git a/lsl_definitions.yaml b/lsl_definitions.yaml index 2de4ad8a..068693f6 100644 --- a/lsl_definitions.yaml +++ b/lsl_definitions.yaml @@ -1964,6 +1964,7 @@ constants: with unlisted content types will return status 415. type: integer value: 8 + value-type: string-multi HTTP_BODY_MAXLENGTH: member-of: [HTTPRequestParam] tooltip: "Sets the maximum (UTF-8 encoded) byte length of the HTTP response @@ -1971,6 +1972,7 @@ constants: bytes, LSO Max: 4096 bytes)." type: integer value: 2 + value-type: integer HTTP_BODY_TRUNCATED: member-of: [HTTPResponseError] tooltip: Indicates the response body truncation point in bytes. @@ -1992,12 +1994,14 @@ constants: JSON block) is reported through the http_response event using that key. type: integer value: 9 + value-type: boolean HTTP_METHOD: member-of: [HTTPRequestParam] tooltip: Specifies the HTTP method ('GET', 'POST', 'PUT', or 'DELETE') for the request. type: integer value: 0 + value-type: string HTTP_MIMETYPE: member-of: [HTTPRequestParam] tooltip: Specifies the request Content-Type MIME type. Text types should define @@ -2005,6 +2009,7 @@ constants: 'application/x-www-form-urlencoded' to emulate HTML forms. type: integer value: 1 + value-type: string HTTP_PRAGMA_NO_CACHE: member-of: [HTTPRequestParam] tooltip: "Controls whether the 'Pragma: no-cache' header is sent with the HTTP @@ -2012,6 +2017,7 @@ constants: FALSE, it is omitted." type: integer value: 6 + value-type: boolean HTTP_USER_AGENT: member-of: [HTTPRequestParam] tooltip: Appends a custom string to the default LSL User-Agent header value. @@ -2019,6 +2025,7 @@ constants: not allowed and will cause a script error; use hyphens instead. type: integer value: 7 + value-type: string HTTP_VERBOSE_THROTTLE: member-of: [HTTPRequestParam] tooltip: If set to TRUE, shouts error messages to DEBUG_CHANNEL if the outgoing @@ -2026,6 +2033,7 @@ constants: suppressed. type: integer value: 4 + value-type: boolean HTTP_VERIFY_CERT: member-of: [HTTPRequestParam] tooltip: If set to TRUE, the server's SSL certificate must be verifiable using a @@ -2033,6 +2041,7 @@ constants: SSL certificate is accepted. type: integer value: 3 + value-type: boolean IMG_USE_BAKED_AUX1: tooltip: '' type: string @@ -17002,6 +17011,15 @@ builder-rulesets: - [ asset, sound ] - [ float, volume ] + http-params: + type: table + enum: HTTPRequestParam + filler-tokens: [http] + lua-type: HttpRequestParams + lua-module: llhttp + lua-fn: request + dispatch-fn: HTTPRequest + particle-params: type: table enum: ParticleParam diff --git a/lsl_definitions/generators/lscript.py b/lsl_definitions/generators/lscript.py index c4ac9163..bf2e5b51 100644 --- a/lsl_definitions/generators/lscript.py +++ b/lsl_definitions/generators/lscript.py @@ -993,10 +993,11 @@ def gen_fluent_builder_descriptors(definitions: LSLDefinitions) -> str: def _inspect_dispatch_fn(dispatch_fn_name: str): """Inspect ll{dispatch_fn_name}'s argument list. - Returns (prefix_args, has_link) where prefix_args is a list of - (name: str, is_link: bool) tuples for each non-list prefix arg in LL order. - Raises ValueError for unsupported signatures (non-integer prefix args, - or list not last). + Returns (prefix_args, suffix_args, has_link, ret_type) where: + prefix_args: list of (name, lsl_type, is_link) for args before the list + suffix_args: list of (name, lsl_type) for args after the list + has_link: whether any prefix arg is a link integer + Raises ValueError if no list argument is found. """ full_name = "ll" + dispatch_fn_name func = definitions.functions.get(full_name) @@ -1004,25 +1005,22 @@ def _inspect_dispatch_fn(dispatch_fn_name: str): raise ValueError(f"dispatch-fn '{full_name}' not found in definitions") args = func.arguments - if not args or args[-1].type != LSLType.LIST: - last = args[-1].type if args else "no args" - raise ValueError(f"{full_name}: last argument must be list, got {last}") + # Find the list argument — it may appear anywhere in the signature. + list_idx = next((i for i, a in enumerate(args) if a.type == LSLType.LIST), None) + if list_idx is None: + raise ValueError(f"{full_name}: no list argument found in signature") - prefix_args = args[:-1] - result = [] + prefix_result = [] has_link = False - for arg in prefix_args: - if arg.type != LSLType.INTEGER: - raise ValueError( - f"{full_name}: unsupported non-integer prefix arg '{arg.name}' " - f"(type {arg.type}). Only integer prefix args are supported; " - f"hand-write this wrapper using slua_fluent_serialize() directly." - ) - is_link = "link" in arg.name.lower() + for arg in args[:list_idx]: + is_link = arg.type == LSLType.INTEGER and "link" in arg.name.lower() if is_link: has_link = True - result.append((arg.name, is_link)) - return result, has_link, func.ret_type + prefix_result.append((arg.name, arg.type, is_link)) + + suffix_result = [(arg.name, arg.type) for arg in args[list_idx + 1 :]] + + return prefix_result, suffix_result, has_link, func.ret_type def _lua_fn_to_cpp_name(lua_fn: str) -> str: """Convert camelCase Lua function name to snake_case C++ variable name. @@ -1031,31 +1029,51 @@ def _lua_fn_to_cpp_name(lua_fn: str) -> str: return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", lua_fn).lower() def _build_wrapper( - cpp_name: str, dispatch_fn: str, prefix_args: list, has_link: bool, ret_type: LSLType + cpp_name: str, + dispatch_fn: str, + prefix_args: list, + suffix_args: list, + has_link: bool, + ret_type: LSLType, ) -> str: """Emit a C++ lambda that reads Lua args, serializes params, and calls ll.dispatch_fn. Lua stack layout: - 1 .. N : non-link integer args (in LL order, skipping link) - N+1 : params table - N+2 : link (optional, if has_link) - LL call order: original LL order (link, non-link args, rules list). + 1 .. P : non-link prefix args (in LL order, skipping link) + P+1 : params table + P+2 .. : suffix args (in LL order) + last : link (optional, if has_link) + LL call order: original LL order (prefix args, rules list, suffix args). """ - non_link_count = sum(1 for _, is_link in prefix_args if not is_link) - params_lua_pos = non_link_count + 1 - link_lua_pos = params_lua_pos + 1 - total_ll_args = len(prefix_args) + 1 # prefix args + rules list + non_link_prefix_count = sum(1 for _, _, is_link in prefix_args if not is_link) + params_lua_pos = non_link_prefix_count + 1 + suffix_start_lua_pos = params_lua_pos + 1 + link_lua_pos = suffix_start_lua_pos + len(suffix_args) + total_ll_args = len(prefix_args) + 1 + len(suffix_args) # prefix + rules + suffix lines = [] - # Read non-link args from Lua stack (positions 1..N, in LL order). + # Read non-link INTEGER prefix args into C variables (positions 1..P, in LL order). + # Non-integer prefix args stay on the Lua stack and are pushed by value later. lua_pos = 1 - for arg_name, is_link in prefix_args: + prefix_lua_positions: dict[str, int] = {} + for arg_name, arg_type, is_link in prefix_args: if not is_link: - lines.append(f" int {arg_name.lower()} = luaL_checkinteger(L, {lua_pos});") + prefix_lua_positions[arg_name] = lua_pos + if arg_type == LSLType.INTEGER: + lines.append(f" int {arg_name.lower()} = luaL_checkinteger(L, {lua_pos});") lua_pos += 1 - # Read optional link arg (last Lua position). + # Similarly for suffix args (start after the params table slot). + lua_pos += 1 # skip params_lua_pos + suffix_lua_positions: dict[str, int] = {} + for arg_name, arg_type in suffix_args: + suffix_lua_positions[arg_name] = lua_pos + if arg_type == LSLType.INTEGER: + lines.append(f" int {arg_name.lower()}_s = luaL_checkinteger(L, {lua_pos});") + lua_pos += 1 + + # Read optional link arg (comes after suffix args on the Lua stack). if has_link: lines.append( f" int link = lua_isnoneornil(L, {link_lua_pos}) " @@ -1069,12 +1087,26 @@ def _build_wrapper( # Dispatch to ll.* in original LL order. lines.append(' lua_rawgetfield(L, LUA_BASEGLOBALSINDEX, "ll");') lines.append(f' lua_rawgetfield(L, -1, "{dispatch_fn}");') - for arg_name, is_link in prefix_args: + + # Push prefix args in LL order. + for arg_name, arg_type, is_link in prefix_args: if is_link: lines.append(" lua_pushinteger(L, link);") - else: + elif arg_type == LSLType.INTEGER: lines.append(f" lua_pushinteger(L, {arg_name.lower()});") + else: + lines.append(f" lua_pushvalue(L, {prefix_lua_positions[arg_name]});") + + # Push the serialized rules list. lines.append(" lua_pushvalue(L, rules_idx);") + + # Push suffix args in LL order. + for arg_name, arg_type in suffix_args: + if arg_type == LSLType.INTEGER: + lines.append(f" lua_pushinteger(L, {arg_name.lower()}_s);") + else: + lines.append(f" lua_pushvalue(L, {suffix_lua_positions[arg_name]});") + nresults = 0 if ret_type == LSLType.VOID else 1 lines.append(f" lua_call(L, {total_ll_args}, {nresults});") lines.append(f" return {nresults};") @@ -1093,12 +1125,7 @@ def _build_wrapper( continue enum_name = ruleset_data["enum"] - lua_module = ruleset_data["lua-module"] - lua_fn = ruleset_data["lua-fn"] - dispatch_fn = ruleset_data["dispatch-fn"] - - prefix_args, has_link, ret_type = _inspect_dispatch_fn(dispatch_fn) - cpp_name = _lua_fn_to_cpp_name(lua_fn) + dispatch_fn = ruleset_data.get("dispatch-fn") # Descriptor array/def names derived from the enum name. # e.g. ParticleParam -> kParticleParamsDescs, kParticleParamsDef @@ -1182,10 +1209,18 @@ def _build_wrapper( f"fluent_builder_def_build({array_name}, std::size({array_name}));\n" ) - section += _build_wrapper(cpp_name, dispatch_fn, prefix_args, has_link, ret_type) + "\n" - section += ( - f'slua_register_fluent_fn(L, "{lua_module}", "{lua_fn}", {cpp_name}, {def_name});' - ) + if dispatch_fn is not None: + lua_module = ruleset_data["lua-module"] + lua_fn = ruleset_data["lua-fn"] + prefix_args, suffix_args, has_link, ret_type = _inspect_dispatch_fn(dispatch_fn) + cpp_name = _lua_fn_to_cpp_name(lua_fn) + section += ( + _build_wrapper(cpp_name, dispatch_fn, prefix_args, suffix_args, has_link, ret_type) + + "\n" + ) + section += ( + f'slua_register_fluent_fn(L, "{lua_module}", "{lua_fn}", {cpp_name}, {def_name});' + ) sections.append(section) diff --git a/lsl_definitions/rulesets.py b/lsl_definitions/rulesets.py index a0956e10..b13c43f7 100644 --- a/lsl_definitions/rulesets.py +++ b/lsl_definitions/rulesets.py @@ -49,6 +49,7 @@ class BuilderParamType: "key": "k", "string-csv": "C", "string-map": "M", + "string-multi": "N", } diff --git a/lsl_definitions/slua.py b/lsl_definitions/slua.py index bbaf730a..4e0ef7ee 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -27,7 +27,8 @@ "key": "(string | uuid)", "asset": "(string | uuid)", "string-csv": "{string}", - "string-map": "{[string]: string}", + "string-map": "{[string]: string | number | boolean | vector | quaternion}", + "string-multi": "{string}", } if TYPE_CHECKING: diff --git a/slua_definitions.yaml b/slua_definitions.yaml index 71d0bf44..bbe44585 100644 --- a/slua_definitions.yaml +++ b/slua_definitions.yaml @@ -247,6 +247,11 @@ type-aliases: export: true definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("prim-media-params") selene-type: table +- name: HttpRequestParams + comment: HTTP request parameter table. Pass to ll.httpRequest() to configure an HTTP request. + export: true + definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("http-params") + selene-type: table classes: - name: LLEvents comment: Event registration and management class for Second Life events @@ -1455,6 +1460,21 @@ modules: comment: A constant to pass for an empty object to json encode. type: '{}' value: 'setmetatable({}, lljson.object_mt)' +- name: llhttp + comment: Utilities for making HTTP requests. + functions: + - name: request + comment: Send an HTTP request. Returns a key identifying the request, which is + delivered via the http_response event. Omit body for requests that do not + need a body (e.g. GET). + parameters: + - name: url + type: string + - name: params + type: HttpRequestParams? + - name: body + type: string? + return-type: uuid - name: llprim comment: Utilities for working with prims / objects constants: From cfb60b61bac07091e95535e6a44146a1a0c14b93 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 23 Jun 2026 16:22:04 +0000 Subject: [PATCH 09/12] update for ruff check. --- lsl_definitions/lsl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lsl_definitions/lsl.py b/lsl_definitions/lsl.py index 291076d0..cfae291b 100644 --- a/lsl_definitions/lsl.py +++ b/lsl_definitions/lsl.py @@ -152,10 +152,10 @@ class LSLConstant: slua_deprecated: Deprecated | None private: bool """Whether this should this be included in the syntax file""" - pretty_name: Optional[str] = None + pretty_name: str | None = None """Optional override for the auto-generated property alias in table-ruleset APIs.""" member_of: list[LSLEnum] = dataclasses.field(default_factory=list) - value_type: Optional[str] = None + value_type: str | None = None @property def value_raw(self) -> str: From c714e01399efdb3085ee29b0cab06ab5d461bf8a Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 23 Jun 2026 11:14:03 -0700 Subject: [PATCH 10/12] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lsl_definitions.schema.json | 2 +- lsl_definitions/slua.py | 1 + slua_definitions.yaml | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lsl_definitions.schema.json b/lsl_definitions.schema.json index bed19841..2350c6fd 100644 --- a/lsl_definitions.schema.json +++ b/lsl_definitions.schema.json @@ -270,7 +270,7 @@ "pattern": "^[A-Za-z][a-zA-Z0-9]*$" }, "dispatch-fn": { - "markdownDescription": "(`table` only) `ll.*` function name whose signature drives code generation (e.g. `LinkParticleSystem`). Must take a list as its last argument.", + "markdownDescription": "(`table` only) `ll.*` function name whose signature drives code generation (e.g. `LinkParticleSystem`). Must take a list argument (not necessarily last).", "type": "string", "pattern": "^[A-Z][A-Za-z0-9]*$" } diff --git a/lsl_definitions/slua.py b/lsl_definitions/slua.py index 4e0ef7ee..818bad40 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -23,6 +23,7 @@ "integer": "number", "boolean": "boolean", "vector": "vector", + "rotation": "rotation", "string": "string", "key": "(string | uuid)", "asset": "(string | uuid)", diff --git a/slua_definitions.yaml b/slua_definitions.yaml index bbe44585..032b8960 100644 --- a/slua_definitions.yaml +++ b/slua_definitions.yaml @@ -238,17 +238,17 @@ type-aliases: definition: "LLJsonDecodeOptionsWithoutPath | LLJsonDecodeOptionsWithPath" selene-type: any - name: ParticleParams - comment: Particle system parameter table. Pass to llprim.ParticleSystem() to emit particles. + comment: Particle system parameter table. Pass to llprim.particleSystem() to emit particles. export: true definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("particle-params") selene-type: table - name: MediaParams - comment: Prim face media parameter table. Pass to llprim.SetMedia() to configure media on a face. + comment: Prim face media parameter table. Pass to llprim.setMedia() to configure media on a face. export: true definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("prim-media-params") selene-type: table - name: HttpRequestParams - comment: HTTP request parameter table. Pass to ll.httpRequest() to configure an HTTP request. + comment: HTTP request parameter table. Pass to llhttp.request() to configure an HTTP request. export: true definition: '{[string]: any}' # rebuilt at generation time from expand_table_ruleset("http-params") selene-type: table From c7a7ae6cf40d237e054cb9200bbd8279a2af1446 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 23 Jun 2026 21:39:12 +0000 Subject: [PATCH 11/12] prepushing generated --- generated/cpp/fluent_builder_descriptors.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generated/cpp/fluent_builder_descriptors.cpp b/generated/cpp/fluent_builder_descriptors.cpp index e54b651d..53eb671a 100644 --- a/generated/cpp/fluent_builder_descriptors.cpp +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -20,7 +20,7 @@ auto request = [](lua_State* L) -> int { lua_rawgetfield(L, -1, "HTTPRequest"); lua_pushvalue(L, 1); lua_pushvalue(L, rules_idx); - lua_pushvalue(L, 3); + if (lua_isnoneornil(L, 3)) lua_pushliteral(L, ""); else lua_pushvalue(L, 3); lua_call(L, 3, 1); return 1; }; From 88240925791b5ce0e893377073c6bf71c1687a7e Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 23 Jun 2026 21:43:41 +0000 Subject: [PATCH 12/12] fix for potential stack underflow --- lsl_definitions/generators/lscript.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lsl_definitions/generators/lscript.py b/lsl_definitions/generators/lscript.py index bf2e5b51..e0504932 100644 --- a/lsl_definitions/generators/lscript.py +++ b/lsl_definitions/generators/lscript.py @@ -1070,7 +1070,7 @@ def _build_wrapper( for arg_name, arg_type in suffix_args: suffix_lua_positions[arg_name] = lua_pos if arg_type == LSLType.INTEGER: - lines.append(f" int {arg_name.lower()}_s = luaL_checkinteger(L, {lua_pos});") + lines.append(f" int {arg_name.lower()}_s = luaL_optinteger(L, {lua_pos}, 0);") lua_pos += 1 # Read optional link arg (comes after suffix args on the Lua stack). @@ -1102,10 +1102,27 @@ def _build_wrapper( # Push suffix args in LL order. for arg_name, arg_type in suffix_args: + pos = suffix_lua_positions[arg_name] if arg_type == LSLType.INTEGER: lines.append(f" lua_pushinteger(L, {arg_name.lower()}_s);") + elif arg_type == LSLType.FLOAT: + lines.append( + f" if (lua_isnoneornil(L, {pos})) lua_pushnumber(L, 0.0); else lua_pushvalue(L, {pos});" + ) + elif arg_type in (LSLType.STRING, LSLType.KEY): + lines.append( + f' if (lua_isnoneornil(L, {pos})) lua_pushliteral(L, ""); else lua_pushvalue(L, {pos});' + ) + elif arg_type == LSLType.VECTOR: + lines.append( + f" if (lua_isnoneornil(L, {pos})) lua_pushvector(L, 0.0f, 0.0f, 0.0f); else lua_pushvalue(L, {pos});" + ) + elif arg_type == LSLType.ROTATION: + lines.append( + f" if (lua_isnoneornil(L, {pos})) luaSL_pushquaternion(L, 0.0f, 0.0f, 0.0f, 1.0f); else lua_pushvalue(L, {pos});" + ) else: - lines.append(f" lua_pushvalue(L, {suffix_lua_positions[arg_name]});") + lines.append(f" lua_pushvalue(L, {pos});") nresults = 0 if ret_type == LSLType.VOID else 1 lines.append(f" lua_call(L, {total_ll_args}, {nresults});")