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..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" @@ -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..53eb671a --- /dev/null +++ b/generated/cpp/fluent_builder_descriptors.cpp @@ -0,0 +1,123 @@ +// 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); + if (lua_isnoneornil(L, 3)) lua_pushliteral(L, ""); else 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}, + {"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}, + {"angle_inner", 'f', 10}, + {"angle_outer", '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}, + {"glow_begin", 'f', 26}, + {"glow_end", 'f', 27}, +}; +static const FluentFlagDescriptor kParticleParamFlagDescs[] = { + {"color_interp", 0x1, 0}, + {"scale_interp", 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}, +}; +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); + +// 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", 'C', 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, 1); + return 1; +}; +slua_register_fluent_fn(L, "llprim", "setMedia", set_media, kPrimMediaParamsDef); \ 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/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 2a53fe0d..c97f3bc6 100644 --- a/generated/lua_keywords_pretty.xml +++ b/generated/lua_keywords_pretty.xml @@ -198,6 +198,21 @@ export type DateTypeResult = { isdst: boolean?, } + HttpRequestParams + + tooltip + HTTP request parameter table. Pass to ll.httpRequest() to configure an HTTP request. + + MediaParams + + tooltip + Prim face media parameter table. Pass to llprim.SetMedia() to configure media on a face. + + ParticleParams + + tooltip + Particle system parameter table. Pass to llprim.ParticleSystem() to emit particles. + list tooltip @@ -2723,7 +2738,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 @@ -5916,7 +5931,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 @@ -13217,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 @@ -13359,6 +13422,79 @@ 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. + + 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 16223968..8aaa19df 100644 --- a/generated/secondlife.d.luau +++ b/generated/secondlife.d.luau @@ -85,6 +85,74 @@ 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?, +} +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?, +} +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 @@ -361,6 +429,15 @@ declare llbase64: { } +--------------------------- +-- Global Table: llhttp +--------------------------- + +declare llhttp: { + request: (url: string, params: HttpRequestParams?, body: string?) -> uuid, +} + + --------------------------- -- Global Table: lljson --------------------------- @@ -385,6 +462,8 @@ 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 4bdf9206..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/" @@ -370,6 +376,12 @@ "@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/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" @@ -5817,7 +5829,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." @@ -6864,7 +6876,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 3ab0c5f6..2a18592c 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 @@ -5449,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. @@ -5507,6 +5520,23 @@ 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. + 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..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)|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)\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 b8369395..6f3fbe89 100644 --- a/generated/syntax/slua.tmLanguage.json +++ b/generated/syntax/slua.tmLanguage.json @@ -362,7 +362,7 @@ }, { "name": "support.function.luau", - "match": "(?- @@ -4078,17 +4096,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 +4140,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: '' @@ -4147,22 +4170,26 @@ 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-csv 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 @@ -4576,11 +4603,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 @@ -4596,33 +4625,42 @@ 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 value: 4 + 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 value: 3 + 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 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 @@ -4639,12 +4677,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 @@ -4655,6 +4695,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 @@ -4666,27 +4707,35 @@ 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 value: 2 + 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 value: 1 + 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 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 +4765,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 +4780,54 @@ 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] + 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 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,19 +4842,23 @@ constants: emission axis for ANGLE and ANGLE_CONE patterns. type: integer value: 21 + 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. 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 +4900,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 @@ -16678,6 +16744,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, @@ -16943,3 +17010,32 @@ builder-rulesets: params: - [ 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 + filler-tokens: [part, src] + flag-enum: ParticleFlag + flag-field: flags + flag-mask: _MASK + lua-module: llprim + 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/generators/lscript.py b/lsl_definitions/generators/lscript.py index 607adc31..e0504932 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 _VALUE_TYPE_TO_SEMANTIC, expand_table_ruleset from lsl_definitions.utils import is_uuid, splice_str, to_c_str @@ -46,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", @@ -596,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 += ( @@ -607,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" ) @@ -615,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: @@ -632,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) @@ -883,6 +975,275 @@ 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 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 = _VALUE_TYPE_TO_SEMANTIC + + def _inspect_dispatch_fn(dispatch_fn_name: str): + """Inspect ll{dispatch_fn_name}'s argument list. + + 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) + if func is None: + raise ValueError(f"dispatch-fn '{full_name}' not found in definitions") + + args = func.arguments + # 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_result = [] + has_link = False + for arg in args[:list_idx]: + is_link = arg.type == LSLType.INTEGER and "link" in arg.name.lower() + if is_link: + has_link = True + 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. + 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, + 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 .. 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_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 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 + prefix_lua_positions: dict[str, int] = {} + for arg_name, arg_type, is_link in prefix_args: + if not is_link: + 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 + + # 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_optinteger(L, {lua_pos}, 0);") + 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}) " + 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}");') + + # 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);") + 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: + 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, {pos});") + + 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 ( + 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": + continue + + enum_name = ruleset_data["enum"] + dispatch_fn = ruleset_data.get("dispatch-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}}},") + + param_body = "\n".join(desc_lines) + section = ( + f"// {ruleset_name}\n" + f"static const FluentParamDescriptor {array_name}[] = {{\n{param_body}\n}};\n" + ) + + 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: + 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"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" + ) + + 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) + + 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 1bc709d1..cfae291b 100644 --- a/lsl_definitions/lsl.py +++ b/lsl_definitions/lsl.py @@ -152,7 +152,10 @@ class LSLConstant: slua_deprecated: Deprecated | None private: bool """Whether this should this be included in the syntax file""" + 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: str | None = None @property def value_raw(self) -> str: @@ -570,14 +573,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 @@ -782,6 +786,8 @@ 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), + 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 af043a77..b13c43f7 100644 --- a/lsl_definitions/rulesets.py +++ b/lsl_definitions/rulesets.py @@ -1,6 +1,7 @@ """ -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 @@ -34,6 +35,23 @@ 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", + "string-multi": "N", +} + @dataclasses.dataclass(frozen=True) class BuilderMethodParam: @@ -63,6 +81,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: str | None + """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 +110,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 +154,102 @@ 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: + 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} " + f"but has no value-type annotation." + ) + 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] + 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. + # A constant's pretty_name field takes priority over the filler-token derivation. + 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] + return "_".join(tokens) if tokens else strict + + 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 (override is None and pretty in colliding) else pretty, + tag=tag, + value_type=vt, + ) + for (strict, tag, vt, override), pretty in zip(rows, pretties) + ] + + +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..818bad40 100644 --- a/lsl_definitions/slua.py +++ b/lsl_definitions/slua.py @@ -10,9 +10,28 @@ 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", + "boolean": "boolean", + "vector": "vector", + "rotation": "rotation", + "string": "string", + "key": "(string | uuid)", + "asset": "(string | uuid)", + "string-csv": "{string}", + "string-map": "{[string]: string | number | boolean | vector | quaternion}", + "string-multi": "{string}", +} + if TYPE_CHECKING: from typing import Literal @@ -418,6 +437,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 +672,78 @@ 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 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) 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(): + if ruleset_data.get("type") != "table": + continue + lua_type = ruleset_data.get("lua-type") + if not lua_type: + continue + + 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") + props.append((prop_name, f"{luau_base}?")) + + # 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: + 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=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: def __init__(self): diff --git a/slua_definitions.yaml b/slua_definitions.yaml index cb5141b5..032b8960 100644 --- a/slua_definitions.yaml +++ b/slua_definitions.yaml @@ -237,6 +237,21 @@ 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 +- 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 +- name: HttpRequestParams + 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 classes: - name: LLEvents comment: Event registration and management class for Second Life events @@ -1445,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: @@ -1452,6 +1482,25 @@ 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: 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: