diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md index 57b1f092..5cecceeb 100644 --- a/docs/scarpet/Full.md +++ b/docs/scarpet/Full.md @@ -1701,11 +1701,251 @@ Returns full lists of keys, values and key-value pairs (2-element lists) for all Here is the gist of the Minecraft related functions. Otherwise the CarpetScript could live without Minecraft. + +## App structure + +The main delivery method for scarpet programs into the game is in the form of apps in `*.sc` files located in the world `scripts` +folder. When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope +(more about the app scopes below), without executing of any functions, unless called directly, and with the exception of the +`__config()` function, if present, which will be executed once. Loading the app will also bind specific +events to the event system (check Events section for details). + +Unloading an app removes all of its state from the game, disables commands, removes bounded events, and +saves its global state. If more cleanup is needed, one can define `__on_close()` function which will be +executed when the module is unloaded, or server is closing or crashing. However, there is no need to do that +explicitly for the things that clean up automatically, as indicated in the previous statement. With `'global'` scoped +apps `__on_close()` will execute once per app, and with `'player'` scoped apps, will execute once per player per app. + +### App config via `__config()` function + +If an app defines `__config` method, and that method returns a map, it will be used to apply custom settings +for this app. Currently the following options are supported: + +* `'scope'`: default scope for global variables for the app, Default is `'player'`, which means that globals and defined +functions will be unique for each player so that apps for each player will run in isolation. This is useful in +tool-like applications, where behaviour of things is always from a player's perspective. With player scope the initial run +of the app creates is initial state: defined functions, global variables, config and event handlers, which is then copied for +each player that interacts with the app. With `'global'` scope - the state created by the initial load is the only variant of +the app state and all players interactions run in the same context, sharing defined functions, globals, config and events. +`'global'` scope is most applicable to world-focused apps, where either players are not relevant, or player data is stored +explicitly keyed with players, player names, uuids, etc. +Even for `'player'` scoped apps, you can access specific player app with with commandblocks using +`/execute as run script in run ...`. +To access global/server state for a player app, you need to disown the command from any player, +so either use a command block, or any +arbitrary entity: `/execute as @e[type=bat,limit=1] run script in globals` for instance, however +running anything in the global scope for a `'player'` scoped app is not recommended. +* `'stay_loaded'`: defaults to `false`. If true, and `/carpet scriptsAutoload` is turned on, the following apps will +stay loaded after startup. Otherwise, after reading the app the first time, and fetching the config, server will drop them down. +This is to allow to store multiple apps on the server/world and selectively decide which one should be running at +startup. WARNING: all apps will run once at startup anyways, so be aware that their actions that are called +statically, will be performed once anyways. +* `'legacy_command_type_support'` - if `true`, and the app defines the legacy command system via `__command()` function, +all parameters of command functions will be interpreted and used using brigadier / vanilla style argument parser and their type +will be inferred from their names, otherwise +the legacy scarpet variable parser will be used to provide arguments to commands. +* `'allow_command_conflicts'` - if custom app commands tree is defined, the app engine will check and identify +conflicts and ambiguities between different paths of execution. While ambiguous commands are allowed in brigadier, +and they tend to execute correctly, the suggestion support works really poorly in these situations and scarpet +will warn and prevent such apps from loading with an error message. If `allow_command_conflicts` is specified and +`true`, then scarpet will load all provided commands regardless. +* `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below +* `'commands'` - defines custom commands for the app to be executed with `/` command, see below + +## Custom app commands + +Apps can register custom commands added to the existing command system under `/` where `` is the +name of the app. There are three ways apps can provide commands: + +### Simple commands without custom argument support + +Synopsis: +``` +__command() -> 'root command' +foo() -> 'running foo'; +bar(a, b) -> a + b; +baz(a, b) -> // same thing +( + print(a+b); + null +) +``` + +If a loaded app contains `__command()` method, it will attempt to register a command with that app name, +and register all public (not starting with an underscore) functions available in the app as subcommands, in the form of +`/ `. Arguments are parsed from a single +`greedy string` brigadier argument, and split into function parameters. Parsing of arguments is limited +to numbers, string constants, and available global variables, whitespace separated. Using functions and operators other than +unary `-`, would be unsafe, so it is not allowed. +In this mode, if a function returns a non-null value, it will be printed as a result to the +invoker (e.g. in chat). If the provided argument list does not match the expected argument count of a function, an error message +will be generated. + +Running the app command that doesn't take any extra arguments, so `/` will run the `__command() -> ` function. + +This mode is best for quick apps that typically don't require any arguments and want to expose some functionality in a +simple and convenient way. + +### Simple commands with vanilla argument type support + +Synopsis: + ``` +__config() -> {'legacy_command_type_support' -> true}; +__command() -> print('root command'); +foo() -> print('running foo'); +add(first_float, other_float) -> print('sum: '+(first_float+other_float)); +bar(entitytype, item) -> print(entitytype+' likes '+item:0); +baz(entities) -> // same thing + ( + print(join(',',entities)); + ) + ``` + +It works similarly to the auto command, but arguments get their inferred types based on the argument +names, looking at the full name, or the suffix after the last `_` that indicates the variable type. For instance, variable named `float` will +be parsed as a floating point number, but it can be named `'first_float'` or `'other_float'` as well. Any variable that is not +supported, will be parsed as a `'string'` type. + +Argument type support includes full support for custom argument types (see below). + +### Custom commands + +Synopsis. This example mimics vanilla `'effect'` command adding extra parameter that is not +available in vanilla - to optionally hide effect icon from UI: +``` +global_instant_effects = {'instant_health', 'instant_damage', 'saturation'}; +__config() -> { + 'commands' -> + { + '' -> _() -> print('this is a root call, does nothing. Just for show'), + 'clear' -> _() -> clear_all([player()]), + 'clear ' -> 'clear_all', + 'clear ' -> 'clear', + 'give ' -> ['apply', -1, 0, false, true], + 'give ' -> ['apply', 0, false, true], + 'give ' -> ['apply', false, true], + 'give ' -> ['apply', true], + 'give ' -> 'apply', + + }, + 'arguments' -> { + 'seconds' -> { 'type' -> 'int', 'min' -> 1, 'max' -> 1000000, 'suggest' -> [60]}, + 'amplifier' -> { 'type' -> 'int', 'min' -> 0, 'max' -> 255, 'suggest' -> [0]}, + 'hideParticles' -> {'type' -> 'bool'}, // pure rename + 'showIcon' -> {'type' -> 'bool'}, // pure rename + } +}; + + +clear_all(targets) -> for(targets, modify(_, 'effect')); +clear(targets, effect) -> for(targets, modify(_, 'effect', effect)); +apply(targets, effect, seconds, amplifier, part, icon) -> +( + ticks = if (has(global_instant_effects, effect), + if (seconds < 0, 1, seconds), + if (seconds < 0, 600, 20*seconds) + ); + for (targets, modify(_, 'effect', effect, ticks, amplifier, part, icon)); +) +``` + +This is the most flexible way to specify custom commands with scarpet. it works by providing command +paths with functions to execute, and optionally, custom argument types. Commands are listed in a map, where +the key (can be empty) consists of +the execution path with the command syntax, which consists of literals (as is) and arguments (wrapped with `<>`), with the name / suffix +of the name of the attribute indicating its type, and the value represent function to call, either function values, +defined function names, or functions with some default arguments. Values extracted from commands will be passed to the +functions and executed. By default, command list will be checked for ambiguities (commands with the same path up to some point +that further use different attributes), causing app loading error if that happens, however this can be suppressed by specifying +`'allow_command_conflicts'`. + +Unlike with legacy command system with types support, names of the arguments and names of the function parameters don't need to match. +The only important aspect is the argument count and argument order. + +Custom commands provide a substantial subset of brigadier features in a simple package, skipping purposely on some less common +and less frequently used features, like forks and redirects, used pretty much only in the vanilla `execute` command. + +### Command argument types + +There are several default argument types that can be used directly without specifying custom types. Each argument can be +customized in the `'arguments'` section of the app config, specifying its base type, via `'type'` that needs +to match any of the built-in types, with a series of optional modifiers. Shared modifiers include: + * `suggest` - static list of suggestions to show above the command while typing + * `suggester` - function taking one map argument, indicating current state of attributes in the parsed command + suggesting a dynamic list of valid suggestions for typing. For instance here is a term based type matching + all loaded players adding Steve and Alex, and since player list changes over time cannot be provided statically: + ``` +__config() -> { + 'arguments' -> { + 'loadedplayer' -> { + 'type' -> 'term', + 'suggester' -> _(args) -> ( + nameset = {'Steve', 'Alex'}; + for(player('all'), nameset += _); + keys(nameset) + ), + } + } +}; + ``` + * `case_sensitive` - whether suggestions are case sensitive, defaults to true + +Here is a list of built-in types, with their return value formats, as well as a list of modifiers + that can be customized for that type (if any) + * `'string'`: a string that can be quoted to include spaces. Customizable with `'options'` - a + static list of valid options it can take. command will fail if the typed string is not in this list. + * `'term'`: single word string, no spaces. Can also be customized with `'options'` + * `'text'`: the rest of the command as a string. Has to be the last argument. Can also be customized with `'options'` + * `'bool'`: `true` or `false` + * `'float'`: a number. Customizable with `'min'` and `'max'` values. + * `'int'`: a number, requiring an integer value. Customizable with `'min'` and `'max'` values. + * `'yaw'`: a number, requiring a valid yaw angle. + * `'pos'`: block position as a triple of coordinates. Customized with `'loaded'`, if true requiring the position + to be loaded. + * `'block'`: a valid block state wrapped in a block value (including block properties and data) + * `'teamcolor'`: name of a team, and an integer color value of one of 16 valid team colors. + * `'columnpos'`: a pair of x and z coordinates. + * `'dimension'`: string representing a valid dimension in the world. + * `'anchor'`: string of `feet` or `eyes`. + * `'entitytype'`: string representing a type of entity + * `'floatrange'`: pair of two numbers where one is smaller than the other + * `'players'`: returning a list of valid player entities. If configured with `'single'` returns only one player or `null`. + * `'intrange'`: same as `'floatrange'`, but requiring integers. + * `'enchantment'`: name of an enchantment + * `'item'`: triple of item type, count of 1, and nbt. + * `'message'`: text with expanded embedded player names + * `'effect'`: string representing a status effect + * `'path'`: a valid nbt path + * `'objective'`: a tuple of scoreboard objective name and its criterion + * `'criterion'`: name of a scoreboard criterion + * `'particle'`: name of a particle + * `'recipe'`: name of a valid recipe. Can be fed to recipe_data function. + * `'advancement'`: name of an advancement + * `'lootcondition'`: a loot condition + * `'loottable'`: name of a loot table source + * `'attribute'`: an attribute name + * `'boss'`: a bossbar name + * `'biome'`: a biome name + * `'sound'`: name of a sound + * `'storekey'`: string of a valid current data store key. + * `'identifier'`: any valid identifier. 'minecraft:' prefix is stripped off as a default. + Configurable with `'options'` parameter providing a static list of valid identifiers. + * `'rotation'`: pair of two numbers indicating yaw and pitch values. + * `'scoreholder'`: list of strings of valid score holders. Customizable with `'single'` that makes the parameter require a single target, retuning `null` if its missing + * `'scoreboardslot'` string representing a valid location of scoreboard display. + * `'swizzle'` - set of axis as a string, sorted. + * `'time'` - number of ticks representing a duration of time. + * `'uuid'` - string of a valid uuid. + * `'surfacelocation'` - pair of x and z coordinates, floating point numbers. + * `'location'` - triple of x, y and z coordinates, optionally centered on the block if + interger coordinates are provided and `'block_centered'` optional modifier is `true`. + ## Dimension warning One note, which is important is that most of the calls for entities and blocks would refer to the current dimension of the caller, meaning, that if we for example list all the players using `player('all')` function, -if a player is in the other dimension, calls to entities and blocks around that player would point incorrectly. +if a player is in the other dimension, calls to entities and blocks around that player would be incorrect. Moreover, running commandblocks in the spawn chunks would mean that commands will always refer to the overworld blocks and entities. In case you would want to run commands across all dimensions, just run three of them, using `/execute in overworld/the_nether/the_end run script run ...` and query players using `player('*')`, @@ -3282,6 +3522,31 @@ entity_load_handler('zombie', _(e) -> schedule(0, _(outer(e)) -> modify(e, 'remo entity_load_handler('zombie', _(e) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'motion', 1.2*e~'motion'))) ``` +Word of caution: entities can be loaded with chunks in various states, for instance when a chunk is being generated, this means +that accessing world blocks would cause the game to freeze due to force generating that chunk while generating the chunk. Make +sure to never assume the chunk is ready and use `entity_load_handler` to schedule actions around the loaded entity, +or manipulate entity directly. + +For instance the following handler is safe, as it only accesses the entity directly. It makes all spawned pigmen jump +``` +/script run entity_load_handler('zombified_piglin', _(e) -> modify(e, 'motion', 0, 1, 0) ) +``` +But the following handler, attempting to despawn pigmen that spawn in portals, will cause the game to freeze due to cascading access to blocks that would cause neighbouring chunks +to force generate, causing also error messages for all pigmen caused by packets send after entity is removed by script. +``` +/script run entity_load_handler('zombified_piglin', _(e) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) +``` +Easiest method to circumvent these issues is delay the check, which may or may not cause cascade load to happen, but +will definitely break the infinite chain. +``` +/script run entity_load_handler('zombified_piglin', _(e) -> schedule(0, _(outer(e)) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ) +``` +But the best is to perform the check first time the entity will be ticked - giving the game all the time to ensure chunk +is fully loaded and entity processing, removing the tick handler +``` +/script run entity_load_handler('zombified_piglin', _(e) -> entity_event(e, 'on_tick', _(e) -> ( if(block(pos(e))=='nether_portal', modify(e, 'remove')); entity_event(e, 'on_tick', null) ) ) ) +``` + ### `entity_event(e, event, function)`, `entity_event(e, event, call_name, ... args?)` Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the @@ -4399,39 +4664,21 @@ bar = 2; Loading operation will load that script code from disk and execute it right away. You would probably use it to load some stored procedures to be used for later. To reload the module, just type `/script load` again. Reloading removes -all the current global state (globals and functions) that were added later by the module. +all the current global state (globals and functions) that were added later by the module. To reload all apps along with +all game resources, use vanilla `/reload` command. + -Loading a module, if it contains a `__command()` method, will attempt to registed a command with that app name, -and register all public (no underscore) functions available in the module as subcommands. It will also bind specific -events to the event system (check Events section for details). Loaded apps have the ability to store and load external files, especially their persistent tag state. For that check `load_app_data` and `store_app_data` functions. -If an app defines `__config` method, and that method returns a map, it will be used to apply custom settings -for this app. Currently the following options are supported: -* `scope`: default scope for global variables for the app, Default is 'player' which means that globals and defined -functions will be unique for each player so that apps for each player will run in isolation. This is useful in -tool-like applications. Another option is 'global' which shares global state for all runs on the server - applicable -to 'block' like solutions, where custom behaviours are applied to blocks. -* `stay_loaded`: defaults to false. If true, and `/carpet scriptsAutoload` is turned on, the following apps will -stay loaded after startup. Otherwise, after reading a code, and fetching the config, server will drop them down. -This is to allow to store multiple apps on the server/world and selectively decide which one should be running at -startup. WARNING: all apps will run once at startup anyways, so be aware that their actions that are called -statically, will be performed once anyways. -Unloading an app removes all of its state from the game, disables commands, and removes bounded events, and -saves its global state. If more cleanup is needed, one can define an `__on_close()` function which will be -executed when the module is unloaded, or server is closing, or crashing. However, there is no need to do that -explicitly for the things mentioned in the previous statement. +Unloading the app will only mask their command tree, not remove it. This has the same effect than not having that command +at all, with the exception that if you load a diffrent app with the same name, this may cause commands to reappear. +To remove the commands fully, use `/reload`. + -Scripts can be loaded in shared(global) and player mode. Default is player, so all globals and stored functions are -individual for each player, meaning scripts don't need to worry about making sure they store some intermittent -data for each player independently. In global mode - all global values and stored functions are shared among all -players. To access specific player data with commandblocks, use `/execute as (player) run script in (app) run ...`. -To access global/server state, you need to disown the command from any player, so use a commandblock, or any -arbitrary entity: `/execute as @e[type=bat,limit=1] run script in (module) globals` for instance. ### `/script in ...` diff --git a/docs/scarpet/api/Entities.md b/docs/scarpet/api/Entities.md index 2cdc4829..794ecc6b 100644 --- a/docs/scarpet/api/Entities.md +++ b/docs/scarpet/api/Entities.md @@ -760,6 +760,31 @@ entity_load_handler('zombie', _(e) -> schedule(0, _(outer(e)) -> modify(e, 'remo entity_load_handler('zombie', _(e) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'motion', 1.2*e~'motion'))) ``` +Word of caution: entities can be loaded with chunks in various states, for instance when a chunk is being generated, this means +that accessing world blocks would cause the game to freeze due to force generating that chunk while generating the chunk. Make +sure to never assume the chunk is ready and use `entity_load_handler` to schedule actions around the loaded entity, +or manipulate entity directly. + +For instance the following handler is safe, as it only accesses the entity directly. It makes all spawned pigmen jump +``` +/script run entity_load_handler('zombified_piglin', _(e) -> modify(e, 'motion', 0, 1, 0) ) +``` +But the following handler, attempting to despawn pigmen that spawn in portals, will cause the game to freeze due to cascading access to blocks that would cause neighbouring chunks +to force generate, causing also error messages for all pigmen caused by packets send after entity is removed by script. +``` +/script run entity_load_handler('zombified_piglin', _(e) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) +``` +Easiest method to circumvent these issues is delay the check, which may or may not cause cascade load to happen, but +will definitely break the infinite chain. +``` +/script run entity_load_handler('zombified_piglin', _(e) -> schedule(0, _(outer(e)) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ) +``` +But the best is to perform the check first time the entity will be ticked - giving the game all the time to ensure chunk +is fully loaded and entity processing, removing the tick handler +``` +/script run entity_load_handler('zombified_piglin', _(e) -> entity_event(e, 'on_tick', _(e) -> ( if(block(pos(e))=='nether_portal', modify(e, 'remove')); entity_event(e, 'on_tick', null) ) ) ) +``` + ### `entity_event(e, event, function)`, `entity_event(e, event, call_name, ... args?)` Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md index 4f6b0e74..1a8142c5 100644 --- a/docs/scarpet/api/Overview.md +++ b/docs/scarpet/api/Overview.md @@ -2,11 +2,251 @@ Here is the gist of the Minecraft related functions. Otherwise the CarpetScript could live without Minecraft. + +## App structure + +The main delivery method for scarpet programs into the game is in the form of apps in `*.sc` files located in the world `scripts` +folder. When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope +(more about the app scopes below), without executing of any functions, unless called directly, and with the exception of the +`__config()` function, if present, which will be executed once. Loading the app will also bind specific +events to the event system (check Events section for details). + +Unloading an app removes all of its state from the game, disables commands, removes bounded events, and +saves its global state. If more cleanup is needed, one can define `__on_close()` function which will be +executed when the module is unloaded, or server is closing or crashing. However, there is no need to do that +explicitly for the things that clean up automatically, as indicated in the previous statement. With `'global'` scoped +apps `__on_close()` will execute once per app, and with `'player'` scoped apps, will execute once per player per app. + +### App config via `__config()` function + +If an app defines `__config` method, and that method returns a map, it will be used to apply custom settings +for this app. Currently the following options are supported: + +* `'scope'`: default scope for global variables for the app, Default is `'player'`, which means that globals and defined +functions will be unique for each player so that apps for each player will run in isolation. This is useful in +tool-like applications, where behaviour of things is always from a player's perspective. With player scope the initial run +of the app creates is initial state: defined functions, global variables, config and event handlers, which is then copied for +each player that interacts with the app. With `'global'` scope - the state created by the initial load is the only variant of +the app state and all players interactions run in the same context, sharing defined functions, globals, config and events. +`'global'` scope is most applicable to world-focused apps, where either players are not relevant, or player data is stored +explicitly keyed with players, player names, uuids, etc. +Even for `'player'` scoped apps, you can access specific player app with with commandblocks using +`/execute as run script in run ...`. +To access global/server state for a player app, you need to disown the command from any player, +so either use a command block, or any +arbitrary entity: `/execute as @e[type=bat,limit=1] run script in globals` for instance, however +running anything in the global scope for a `'player'` scoped app is not recommended. +* `'stay_loaded'`: defaults to `false`. If true, and `/carpet scriptsAutoload` is turned on, the following apps will +stay loaded after startup. Otherwise, after reading the app the first time, and fetching the config, server will drop them down. +This is to allow to store multiple apps on the server/world and selectively decide which one should be running at +startup. WARNING: all apps will run once at startup anyways, so be aware that their actions that are called +statically, will be performed once anyways. +* `'legacy_command_type_support'` - if `true`, and the app defines the legacy command system via `__command()` function, +all parameters of command functions will be interpreted and used using brigadier / vanilla style argument parser and their type +will be inferred from their names, otherwise +the legacy scarpet variable parser will be used to provide arguments to commands. +* `'allow_command_conflicts'` - if custom app commands tree is defined, the app engine will check and identify +conflicts and ambiguities between different paths of execution. While ambiguous commands are allowed in brigadier, +and they tend to execute correctly, the suggestion support works really poorly in these situations and scarpet +will warn and prevent such apps from loading with an error message. If `allow_command_conflicts` is specified and +`true`, then scarpet will load all provided commands regardless. +* `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below +* `'commands'` - defines custom commands for the app to be executed with `/` command, see below + +## Custom app commands + +Apps can register custom commands added to the existing command system under `/` where `` is the +name of the app. There are three ways apps can provide commands: + +### Simple commands without custom argument support + +Synopsis: +``` +__command() -> 'root command' +foo() -> 'running foo'; +bar(a, b) -> a + b; +baz(a, b) -> // same thing +( + print(a+b); + null +) +``` + +If a loaded app contains `__command()` method, it will attempt to register a command with that app name, +and register all public (not starting with an underscore) functions available in the app as subcommands, in the form of +`/ `. Arguments are parsed from a single +`greedy string` brigadier argument, and split into function parameters. Parsing of arguments is limited +to numbers, string constants, and available global variables, whitespace separated. Using functions and operators other than +unary `-`, would be unsafe, so it is not allowed. +In this mode, if a function returns a non-null value, it will be printed as a result to the +invoker (e.g. in chat). If the provided argument list does not match the expected argument count of a function, an error message +will be generated. + +Running the app command that doesn't take any extra arguments, so `/` will run the `__command() -> ` function. + +This mode is best for quick apps that typically don't require any arguments and want to expose some functionality in a +simple and convenient way. + +### Simple commands with vanilla argument type support + +Synopsis: + ``` +__config() -> {'legacy_command_type_support' -> true}; +__command() -> print('root command'); +foo() -> print('running foo'); +add(first_float, other_float) -> print('sum: '+(first_float+other_float)); +bar(entitytype, item) -> print(entitytype+' likes '+item:0); +baz(entities) -> // same thing + ( + print(join(',',entities)); + ) + ``` + +It works similarly to the auto command, but arguments get their inferred types based on the argument +names, looking at the full name, or the suffix after the last `_` that indicates the variable type. For instance, variable named `float` will +be parsed as a floating point number, but it can be named `'first_float'` or `'other_float'` as well. Any variable that is not +supported, will be parsed as a `'string'` type. + +Argument type support includes full support for custom argument types (see below). + +### Custom commands + +Synopsis. This example mimics vanilla `'effect'` command adding extra parameter that is not +available in vanilla - to optionally hide effect icon from UI: +``` +global_instant_effects = {'instant_health', 'instant_damage', 'saturation'}; +__config() -> { + 'commands' -> + { + '' -> _() -> print('this is a root call, does nothing. Just for show'), + 'clear' -> _() -> clear_all([player()]), + 'clear ' -> 'clear_all', + 'clear ' -> 'clear', + 'give ' -> ['apply', -1, 0, false, true], + 'give ' -> ['apply', 0, false, true], + 'give ' -> ['apply', false, true], + 'give ' -> ['apply', true], + 'give ' -> 'apply', + + }, + 'arguments' -> { + 'seconds' -> { 'type' -> 'int', 'min' -> 1, 'max' -> 1000000, 'suggest' -> [60]}, + 'amplifier' -> { 'type' -> 'int', 'min' -> 0, 'max' -> 255, 'suggest' -> [0]}, + 'hideParticles' -> {'type' -> 'bool'}, // pure rename + 'showIcon' -> {'type' -> 'bool'}, // pure rename + } +}; + + +clear_all(targets) -> for(targets, modify(_, 'effect')); +clear(targets, effect) -> for(targets, modify(_, 'effect', effect)); +apply(targets, effect, seconds, amplifier, part, icon) -> +( + ticks = if (has(global_instant_effects, effect), + if (seconds < 0, 1, seconds), + if (seconds < 0, 600, 20*seconds) + ); + for (targets, modify(_, 'effect', effect, ticks, amplifier, part, icon)); +) +``` + +This is the most flexible way to specify custom commands with scarpet. it works by providing command +paths with functions to execute, and optionally, custom argument types. Commands are listed in a map, where +the key (can be empty) consists of +the execution path with the command syntax, which consists of literals (as is) and arguments (wrapped with `<>`), with the name / suffix +of the name of the attribute indicating its type, and the value represent function to call, either function values, +defined function names, or functions with some default arguments. Values extracted from commands will be passed to the +functions and executed. By default, command list will be checked for ambiguities (commands with the same path up to some point +that further use different attributes), causing app loading error if that happens, however this can be suppressed by specifying +`'allow_command_conflicts'`. + +Unlike with legacy command system with types support, names of the arguments and names of the function parameters don't need to match. +The only important aspect is the argument count and argument order. + +Custom commands provide a substantial subset of brigadier features in a simple package, skipping purposely on some less common +and less frequently used features, like forks and redirects, used pretty much only in the vanilla `execute` command. + +### Command argument types + +There are several default argument types that can be used directly without specifying custom types. Each argument can be +customized in the `'arguments'` section of the app config, specifying its base type, via `'type'` that needs +to match any of the built-in types, with a series of optional modifiers. Shared modifiers include: + * `suggest` - static list of suggestions to show above the command while typing + * `suggester` - function taking one map argument, indicating current state of attributes in the parsed command + suggesting a dynamic list of valid suggestions for typing. For instance here is a term based type matching + all loaded players adding Steve and Alex, and since player list changes over time cannot be provided statically: + ``` +__config() -> { + 'arguments' -> { + 'loadedplayer' -> { + 'type' -> 'term', + 'suggester' -> _(args) -> ( + nameset = {'Steve', 'Alex'}; + for(player('all'), nameset += _); + keys(nameset) + ), + } + } +}; + ``` + * `case_sensitive` - whether suggestions are case sensitive, defaults to true + +Here is a list of built-in types, with their return value formats, as well as a list of modifiers + that can be customized for that type (if any) + * `'string'`: a string that can be quoted to include spaces. Customizable with `'options'` - a + static list of valid options it can take. command will fail if the typed string is not in this list. + * `'term'`: single word string, no spaces. Can also be customized with `'options'` + * `'text'`: the rest of the command as a string. Has to be the last argument. Can also be customized with `'options'` + * `'bool'`: `true` or `false` + * `'float'`: a number. Customizable with `'min'` and `'max'` values. + * `'int'`: a number, requiring an integer value. Customizable with `'min'` and `'max'` values. + * `'yaw'`: a number, requiring a valid yaw angle. + * `'pos'`: block position as a triple of coordinates. Customized with `'loaded'`, if true requiring the position + to be loaded. + * `'block'`: a valid block state wrapped in a block value (including block properties and data) + * `'teamcolor'`: name of a team, and an integer color value of one of 16 valid team colors. + * `'columnpos'`: a pair of x and z coordinates. + * `'dimension'`: string representing a valid dimension in the world. + * `'anchor'`: string of `feet` or `eyes`. + * `'entitytype'`: string representing a type of entity + * `'floatrange'`: pair of two numbers where one is smaller than the other + * `'players'`: returning a list of valid player entities. If configured with `'single'` returns only one player or `null`. + * `'intrange'`: same as `'floatrange'`, but requiring integers. + * `'enchantment'`: name of an enchantment + * `'item'`: triple of item type, count of 1, and nbt. + * `'message'`: text with expanded embedded player names + * `'effect'`: string representing a status effect + * `'path'`: a valid nbt path + * `'objective'`: a tuple of scoreboard objective name and its criterion + * `'criterion'`: name of a scoreboard criterion + * `'particle'`: name of a particle + * `'recipe'`: name of a valid recipe. Can be fed to recipe_data function. + * `'advancement'`: name of an advancement + * `'lootcondition'`: a loot condition + * `'loottable'`: name of a loot table source + * `'attribute'`: an attribute name + * `'boss'`: a bossbar name + * `'biome'`: a biome name + * `'sound'`: name of a sound + * `'storekey'`: string of a valid current data store key. + * `'identifier'`: any valid identifier. 'minecraft:' prefix is stripped off as a default. + Configurable with `'options'` parameter providing a static list of valid identifiers. + * `'rotation'`: pair of two numbers indicating yaw and pitch values. + * `'scoreholder'`: list of strings of valid score holders. Customizable with `'single'` that makes the parameter require a single target, retuning `null` if its missing + * `'scoreboardslot'` string representing a valid location of scoreboard display. + * `'swizzle'` - set of axis as a string, sorted. + * `'time'` - number of ticks representing a duration of time. + * `'uuid'` - string of a valid uuid. + * `'surfacelocation'` - pair of x and z coordinates, floating point numbers. + * `'location'` - triple of x, y and z coordinates, optionally centered on the block if + interger coordinates are provided and `'block_centered'` optional modifier is `true`. + ## Dimension warning One note, which is important is that most of the calls for entities and blocks would refer to the current dimension of the caller, meaning, that if we for example list all the players using `player('all')` function, -if a player is in the other dimension, calls to entities and blocks around that player would point incorrectly. +if a player is in the other dimension, calls to entities and blocks around that player would be incorrect. Moreover, running commandblocks in the spawn chunks would mean that commands will always refer to the overworld blocks and entities. In case you would want to run commands across all dimensions, just run three of them, using `/execute in overworld/the_nether/the_end run script run ...` and query players using `player('*')`, diff --git a/docs/scarpet/api/ScriptCommand.md b/docs/scarpet/api/ScriptCommand.md index 0ac2ff36..b10b5dc6 100644 --- a/docs/scarpet/api/ScriptCommand.md +++ b/docs/scarpet/api/ScriptCommand.md @@ -28,39 +28,21 @@ bar = 2; Loading operation will load that script code from disk and execute it right away. You would probably use it to load some stored procedures to be used for later. To reload the module, just type `/script load` again. Reloading removes -all the current global state (globals and functions) that were added later by the module. +all the current global state (globals and functions) that were added later by the module. To reload all apps along with +all game resources, use vanilla `/reload` command. + -Loading a module, if it contains a `__command()` method, will attempt to registed a command with that app name, -and register all public (no underscore) functions available in the module as subcommands. It will also bind specific -events to the event system (check Events section for details). Loaded apps have the ability to store and load external files, especially their persistent tag state. For that check `load_app_data` and `store_app_data` functions. -If an app defines `__config` method, and that method returns a map, it will be used to apply custom settings -for this app. Currently the following options are supported: -* `scope`: default scope for global variables for the app, Default is 'player' which means that globals and defined -functions will be unique for each player so that apps for each player will run in isolation. This is useful in -tool-like applications. Another option is 'global' which shares global state for all runs on the server - applicable -to 'block' like solutions, where custom behaviours are applied to blocks. -* `stay_loaded`: defaults to false. If true, and `/carpet scriptsAutoload` is turned on, the following apps will -stay loaded after startup. Otherwise, after reading a code, and fetching the config, server will drop them down. -This is to allow to store multiple apps on the server/world and selectively decide which one should be running at -startup. WARNING: all apps will run once at startup anyways, so be aware that their actions that are called -statically, will be performed once anyways. -Unloading an app removes all of its state from the game, disables commands, and removes bounded events, and -saves its global state. If more cleanup is needed, one can define an `__on_close()` function which will be -executed when the module is unloaded, or server is closing, or crashing. However, there is no need to do that -explicitly for the things mentioned in the previous statement. +Unloading the app will only mask their command tree, not remove it. This has the same effect than not having that command +at all, with the exception that if you load a diffrent app with the same name, this may cause commands to reappear. +To remove the commands fully, use `/reload`. + -Scripts can be loaded in shared(global) and player mode. Default is player, so all globals and stored functions are -individual for each player, meaning scripts don't need to worry about making sure they store some intermittent -data for each player independently. In global mode - all global values and stored functions are shared among all -players. To access specific player data with commandblocks, use `/execute as (player) run script in (app) run ...`. -To access global/server state, you need to disown the command from any player, so use a commandblock, or any -arbitrary entity: `/execute as @e[type=bat,limit=1] run script in (module) globals` for instance. ### `/script in ...` diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java index ec0a2f1a..eebda1d6 100644 --- a/src/main/java/carpet/script/CarpetScriptHost.java +++ b/src/main/java/carpet/script/CarpetScriptHost.java @@ -5,6 +5,7 @@ import carpet.script.api.Auxiliary; import carpet.script.argument.FunctionArgument; import carpet.script.bundled.Module; import carpet.script.command.CommandArgument; +import carpet.script.command.CommandToken; import carpet.script.exception.CarpetExpressionException; import carpet.script.exception.ExpressionException; import carpet.script.exception.InternalExpressionException; @@ -16,24 +17,32 @@ import carpet.script.value.StringValue; import carpet.script.value.Value; import carpet.CarpetSettings; import carpet.utils.Messenger; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; import net.minecraft.nbt.Tag; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.LiteralText; import net.minecraft.util.math.BlockPos; +import org.apache.commons.lang3.tuple.Pair; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static java.lang.Math.max; +import static net.minecraft.server.command.CommandManager.literal; public class CarpetScriptHost extends ScriptHost { @@ -100,6 +109,62 @@ public class CarpetScriptHost extends ScriptHost return host; } + private static int execute(CommandContext ctx, String hostName, FunctionArgument funcSpec, List paramNames) throws CommandSyntaxException + { + CarpetScriptHost cHost = CarpetServer.scriptServer.modules.get(hostName).retrieveForExecution(ctx.getSource()); + List argNames = funcSpec.function.getArguments(); + if ((argNames.size()-funcSpec.args.size()) != paramNames.size()) + throw new SimpleCommandExceptionType(new LiteralText("Target function "+funcSpec.function.getPrettyString()+" as wrong number of arguments, required "+paramNames.size()+", found "+argNames.size()+" with "+funcSpec.args.size()+" provided")).create(); + List args = new ArrayList<>(argNames.size()); + for (String s : paramNames) + { + args.add(CommandArgument.getValue(ctx, s, cHost)); + } + args.addAll(funcSpec.args); + Value response = cHost.handleCommand(ctx.getSource(), funcSpec.function, args); + // will skip prints for new + //if (!response.isNull()) Messenger.m(ctx.getSource(), "gi " + response.getString()); + return (int) response.readInteger(); + } + + public LiteralArgumentBuilder addPathToCommand( + LiteralArgumentBuilder command, + List path, + FunctionArgument functionSpec + ) throws CommandSyntaxException + { + String hostName = main.getName(); + List commandArgs = path.stream().filter(t -> t.isArgument).map(t -> t.surface).collect(Collectors.toList()); + if (commandArgs.size() != (functionSpec.function.getNumParams()-functionSpec.args.size()) ) + throw CommandArgument.error("Number of parameters in function doesn't match parameters for a command"); + if (path.isEmpty()) + { + return command.executes((c) -> execute(c, hostName, functionSpec, Collections.emptyList())); + } + List reversedPath = new ArrayList<>(path); + Collections.reverse(reversedPath); + ArgumentBuilder argChain = reversedPath.get(0).getCommandNode(this).executes(c -> execute(c, hostName, functionSpec, commandArgs)); + for (int i = 1; i < reversedPath.size(); i++) + { + argChain = reversedPath.get(i).getCommandNode(this).then(argChain); + } + return command.then(argChain); + } + + public LiteralArgumentBuilder getNewCommandTree( + List,FunctionArgument>> entries + ) throws CommandSyntaxException + { + String hostName = main.getName(); + LiteralArgumentBuilder command = literal(hostName). + requires((player) -> CarpetServer.scriptServer.modules.containsKey(hostName)); + for (Pair,FunctionArgument> commandData : entries) + { + command = this.addPathToCommand(command, commandData.getKey(), commandData.getValue()); + } + return command; + } + @Override protected ScriptHost duplicate() { @@ -150,7 +215,7 @@ public class CarpetScriptHost extends ScriptHost Value spec = typeData.getValue(); if (!(spec instanceof MapValue)) throw new InternalExpressionException("Spec for '"+argument+"' should be a map"); Map specData = ((MapValue) spec).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); - appArgTypes.put(argument, CommandArgument.buildFromConfig(argument, specData)); + appArgTypes.put(argument, CommandArgument.buildFromConfig(argument, specData, this)); } } appConfig = config; @@ -162,24 +227,59 @@ public class CarpetScriptHost extends ScriptHost return true; } - private boolean readCommands() + static class ListComparator> implements Comparator,?>> + { + @Override + public int compare(Pair,?> p1, Pair,?> p2) { + List o1 = p1.getKey(); + List o2 = p2.getKey(); + for (int i = 0; i < Math.min(o1.size(), o2.size()); i++) { + int c = o1.get(i).compareTo(o2.get(i)); + if (c != 0) { + return c; + } + } + return Integer.compare(o1.size(), o2.size()); + } + } + + public LiteralArgumentBuilder readCommands() throws CommandSyntaxException { Value commands = appConfig.get(StringValue.of("commands")); - if (commands == null) return true; + + if (commands == null) return null; if (!(commands instanceof MapValue)) - throw new InternalExpressionException("'commands' element in config should be a map"); - for (Map.Entry commandsData : ((MapValue)commands).getMap().entrySet()) + throw CommandArgument.error("'commands' element in config should be a map"); + List,FunctionArgument>> commandEntries = new ArrayList<>(); + + for (Map.Entry commandsData : ((MapValue)commands).getMap().entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList())) { - String path = commandsData.getKey().getString(); + List elements = CommandToken.parseSpec(commandsData.getKey().getString(), this); FunctionArgument funSpec = FunctionArgument.fromCommandSpec(this, commandsData.getValue()); - //if (fun) - - //... WIP - - //Map specData = ((MapValue) spec).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); - //appArgTypes.put(argument, CommandArgument.buildFromConfig(argument, specData)); + commandEntries.add(Pair.of(elements, funSpec)); } - return true; + commandEntries.sort(new ListComparator<>()); + if (!appConfig.getOrDefault(StringValue.of("allow_command_conflicts"), Value.FALSE).getBoolean()) + { + for (int i = 0; i < commandEntries.size()-1; i++) + { + List first = commandEntries.get(i).getKey(); + List other = commandEntries.get(i+1).getKey(); + int checkSize = Math.min(first.size(), other.size()); + for (int t = 0; t < checkSize; t++) + { + CommandToken tik = first.get(t); + CommandToken tok = other.get(t); + if (tik.isArgument && tok.isArgument && !tik.surface.equals(tok.surface)) + throw CommandArgument.error("Conflicting commands: \n"+ + " - [" +first.stream().map(tt -> tt.surface).collect(Collectors.joining(" ")) + "] at "+tik.surface+"\n"+ + " - [" +other.stream().map(tt -> tt.surface).collect(Collectors.joining(" ")) + "] at "+tok.surface+"\n"); + if (!tik.equals(tok)) + break; + } + } + } + return this.getNewCommandTree(commandEntries); } @Override diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index 0a299aba..58351dd9 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -1,11 +1,12 @@ package carpet.script; +import carpet.script.argument.FunctionArgument; import carpet.script.bundled.BundledModule; import carpet.CarpetSettings; import carpet.CarpetServer; import carpet.script.bundled.FileModule; import carpet.script.bundled.Module; -import carpet.script.command.CommandArgument; +import carpet.script.command.CommandToken; import carpet.script.exception.InvalidCallbackException; import carpet.script.value.FunctionValue; import carpet.script.value.StringValue; @@ -13,21 +14,16 @@ import carpet.script.value.Value; import carpet.utils.Messenger; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.tree.CommandNode; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.LiteralText; import net.minecraft.util.WorldSavePath; import net.minecraft.util.math.BlockPos; import java.io.File; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -240,7 +236,31 @@ public class CarpetScriptServer return false; } //addEvents(source, name); - if (!addLegacyCommand(source, name, reload, !isRuleApp)) // this needs to be moved to config reader, only supporting legacy command here + String action = reload?"reloaded":"loaded"; + if (newHost.appConfig.get(StringValue.of("commands")) != null) + { + try + { + LiteralArgumentBuilder command = newHost.readCommands(); + if (command != null) + { + if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action+" with /"+name+" command"); + server.getCommandManager().getDispatcher().register(command); + CarpetServer.settingsManager.notifyPlayersCommandsChanged(); + } + else { + if (!isRuleApp) Messenger.m(source, "gi "+name+" app "+action); + } + } + catch (CommandSyntaxException cse) + { + removeScriptHost(source, name, false, false); + Messenger.m(source, "r Failed to build command system for "+name+" thus failed to load the app ", cse.getRawMessage()); + return false; + } + + } + else if (!addLegacyCommand(source, name, reload, !isRuleApp)) // this needs to be moved to config reader, only supporting legacy command here { removeScriptHost(source, name, false, false); Messenger.m(source, "r Failed to build command system for "+name+" thus failed to load the app"); @@ -283,11 +303,16 @@ public class CarpetScriptServer for (String function : host.globaFunctionNames(host.main, s -> !s.startsWith("_")).sorted().collect(Collectors.toList())) { - if (host.appConfig.containsKey(StringValue.of("legacy_command_type_support"))) // temporary stuff for testing + if (host.appConfig.getOrDefault(StringValue.of("legacy_command_type_support"), Value.FALSE).getBoolean()) { try { - command = getFancyCommand(command, host, host.getFunction(function)); + FunctionValue functionValue = host.getFunction(function); + command = host.addPathToCommand( + command, + CommandToken.parseSpec(CommandToken.specFromSignature(functionValue), host), + FunctionArgument.fromCommandSpec(host, functionValue) + ); } catch (CommandSyntaxException e) { @@ -320,49 +345,6 @@ public class CarpetScriptServer return true; } - private int execute(CommandContext ctx, String hostName, FunctionValue function, List paramNames) throws CommandSyntaxException - { - CarpetScriptHost cHost = modules.get(hostName).retrieveForExecution(ctx.getSource()); - List argNames = function.getArguments(); - if (argNames.size() != paramNames.size()) - throw new SimpleCommandExceptionType(new LiteralText("Target function "+function.getPrettyString()+" as wrong number of arguments, required "+paramNames.size()+", found "+argNames.size())).create(); - List args = new ArrayList<>(argNames.size()); - for (String s : paramNames) - { - args.add(CommandArgument.getValue(ctx, s, cHost)); - } - Value response = cHost.handleCommand(ctx.getSource(), function, args); - if (!response.isNull()) Messenger.m(ctx.getSource(), "gi " + response.getString()); - return (int) response.readInteger(); - } - - private LiteralArgumentBuilder getFancyCommand(LiteralArgumentBuilder command, CarpetScriptHost host, FunctionValue function ) throws CommandSyntaxException - { - List argNames = function.getArguments(); - String hostName = host.getName(); - if (argNames.size() == 0) - { - return command.then(literal(function.getString()). - requires((player) -> modules.containsKey(hostName)). - executes((c) -> execute(c, hostName, function, Collections.emptyList()))); - } - else - { - List reversedArgs = new ArrayList<>(argNames); - Collections.reverse(reversedArgs); - RequiredArgumentBuilder argChain = CommandArgument.argumentNode(reversedArgs.get(0), host).executes(c -> execute(c, hostName, function, argNames)); - for (int i = 1; i < reversedArgs.size(); i++) - { - argChain = CommandArgument.argumentNode(reversedArgs.get(i), host).then(argChain); - } - return command.then(literal(function.getString()). - requires((player) -> modules.containsKey(hostName)). - then(argChain) - ); - } - } - - public boolean removeScriptHost(ServerCommandSource source, String name, boolean notifySource, boolean isRuleApp) { diff --git a/src/main/java/carpet/script/argument/FunctionArgument.java b/src/main/java/carpet/script/argument/FunctionArgument.java index 928dd58a..b942e7f7 100644 --- a/src/main/java/carpet/script/argument/FunctionArgument.java +++ b/src/main/java/carpet/script/argument/FunctionArgument.java @@ -85,7 +85,7 @@ public class FunctionArgument extends Argument } else { - String name = funSpec.getString(); + String name = first.getString(); function = host.getAssertFunction(host.main, name); } if (params.size() > 1) args = params.subList(1,params.size()); diff --git a/src/main/java/carpet/script/command/CommandArgument.java b/src/main/java/carpet/script/command/CommandArgument.java index b65e2cc6..82bd5c69 100644 --- a/src/main/java/carpet/script/command/CommandArgument.java +++ b/src/main/java/carpet/script/command/CommandArgument.java @@ -2,11 +2,13 @@ package carpet.script.command; import carpet.fakes.BlockStateArgumentInterface; import carpet.script.CarpetScriptHost; +import carpet.script.argument.FunctionArgument; import carpet.script.exception.InternalExpressionException; import carpet.script.value.BlockValue; import carpet.script.value.EntityValue; import carpet.script.value.FormattedTextValue; import carpet.script.value.ListValue; +import carpet.script.value.MapValue; import carpet.script.value.NBTSerializableValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; @@ -22,11 +24,14 @@ import com.mojang.brigadier.arguments.LongArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.ParsedCommandNode; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; import net.minecraft.advancement.Advancement; import net.minecraft.command.CommandSource; import net.minecraft.command.argument.AngleArgumentType; @@ -74,8 +79,10 @@ import net.minecraft.util.math.Direction; import net.minecraft.util.math.Vec2f; import net.minecraft.util.registry.Registry; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -89,6 +96,11 @@ import static net.minecraft.server.command.CommandManager.argument; public abstract class CommandArgument { + public static CommandSyntaxException error(String text) + { + return new SimpleCommandExceptionType(new LiteralText(text)).create(); + } + private static final List baseTypes = Lists.newArrayList( // default new StringArgument(), @@ -241,10 +253,10 @@ public abstract class CommandArgument public static final CommandArgument DEFAULT = baseTypes.get(0); - private static CommandArgument getTypeForArgument(String argument, CarpetScriptHost host) + public static CommandArgument getTypeForArgument(String argument, CarpetScriptHost host) { String[] components = argument.split("_"); - String suffix = components[components.length-1].toLowerCase(Locale.ROOT); + String suffix = components[components.length-1]; CommandArgument arg = host.appArgTypes.get(suffix); if (arg != null) return arg; return builtIns.getOrDefault(suffix, DEFAULT); @@ -254,13 +266,16 @@ public abstract class CommandArgument { CommandArgument arg = getTypeForArgument(param, host); if (arg.suggestionProvider != null) return argument(param, arg.getArgumentType()).suggests(arg.suggestionProvider); - return arg.needsMatching? argument(param, arg.getArgumentType()).suggests(arg::suggest) : argument(param, arg.getArgumentType()); + return arg.needsMatching? argument(param, arg.getArgumentType()).suggests((c, b) -> arg.suggest(c, b, host)) : argument(param, arg.getArgumentType()); } protected String suffix; protected Collection examples; - protected boolean needsMatching = false; + protected boolean needsMatching; + protected boolean caseSensitive = true; protected SuggestionProvider suggestionProvider; + protected FunctionArgument customSuggester; + protected CommandArgument( String suffix, @@ -287,23 +302,29 @@ public abstract class CommandArgument return suffix; } - public static CommandArgument buildFromConfig(String suffix, Map config) + public static CommandArgument buildFromConfig(String suffix, Map config, CarpetScriptHost host) { if (!config.containsKey("type")) - throw new InternalExpressionException("Custom types should at least specify the base type"); + throw new InternalExpressionException("Custom types should at least specify the type"); String baseType = config.get("type").getString(); if (!builtIns.containsKey(baseType)) throw new InternalExpressionException("Unknown base type: "+baseType); - CommandArgument variant = builtIns.get(baseType).builder().get(); - variant.configure(config); + CommandArgument variant = builtIns.get(baseType).factory().get(); + variant.configure(config, host); variant.suffix = suffix; return variant; } - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { + caseSensitive = config.getOrDefault("case_sensitive", Value.TRUE).getBoolean(); + if (config.containsKey("suggester")) + { + customSuggester = FunctionArgument.fromCommandSpec(host, config.get("suggester")); + } if (config.containsKey("suggest")) { + if (config.containsKey("suggester")) throw new InternalExpressionException("Attempted to provide 'suggest' list while 'suggester' is present"); Value suggestionValue = config.get("suggest"); if (!(suggestionValue instanceof ListValue)) throw new InternalExpressionException("Argument suggestions needs to be a list"); examples = ((ListValue) suggestionValue).getItems().stream() @@ -311,34 +332,59 @@ public abstract class CommandArgument .collect(Collectors.toSet()); if (!examples.isEmpty()) needsMatching = true; } - }; + } public CompletableFuture suggest( CommandContext context, - SuggestionsBuilder suggestionsBuilder - ) + SuggestionsBuilder suggestionsBuilder, + CarpetScriptHost host + ) throws CommandSyntaxException { - String prefix = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT); - suggestFor(prefix).forEach(suggestionsBuilder::suggest); + String prefix = suggestionsBuilder.getRemaining(); + if (!caseSensitive) prefix = prefix.toLowerCase(Locale.ROOT); + suggestFor(context, prefix, host).forEach(suggestionsBuilder::suggest); return suggestionsBuilder.buildFuture(); } - protected List suggestFor(String prefix) + protected List suggestFor(CommandContext context, String prefix, CarpetScriptHost host) throws CommandSyntaxException { - return getOptions().stream().filter(s -> optionMatchesPrefix(prefix, s)).collect(Collectors.toList()); + return getOptions(context, host).stream().filter(s -> optionMatchesPrefix(prefix, s)).collect(Collectors.toList()); } - protected Collection getOptions() + protected Collection getOptions(CommandContext context, CarpetScriptHost host) throws CommandSyntaxException { + if (customSuggester != null) + { + Map params = new HashMap<>(); + for(ParsedCommandNode pnode : context.getNodes()) + { + CommandNode node = pnode.getNode(); + if (node instanceof ArgumentCommandNode) + { + params.put(StringValue.of(node.getName()), CommandArgument.getValue(context, node.getName(), host)); + } + } + List args = new ArrayList<>(customSuggester.args.size()+1); + args.add(MapValue.wrap(params)); + args.addAll(customSuggester.args); + Value response = host.handleCommand(context.getSource(), customSuggester.function, args); + if (!(response instanceof ListValue)) throw error("Custom suggester should return a list of options"); + return ((ListValue) response).getItems().stream().map(Value::getString).collect(Collectors.toList()); + } + if (needsMatching) return examples; //return Lists.newArrayList(""); // better than nothing I guess // nothing is such a bad default. - if (needsMatching) return examples; return Collections.singletonList("... "+getTypeSuffix()); } protected boolean optionMatchesPrefix(String prefix, String option) { + if (!caseSensitive) + { + //prefix = prefix.toLowerCase(Locale.ROOT); + option = option.toLowerCase(Locale.ROOT); + } for(int i = 0; !option.startsWith(prefix, i); ++i) { i = option.indexOf('_', i); @@ -347,12 +393,11 @@ public abstract class CommandArgument return true; } - protected abstract Supplier builder(); + protected abstract Supplier factory(); private static class StringArgument extends CommandArgument { Set validOptions = Collections.emptySet(); - boolean caseSensitive = false; private StringArgument() { super("string", StringArgumentType.StringType.QUOTABLE_PHRASE.getExamples(), true); @@ -377,25 +422,27 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); - caseSensitive = config.getOrDefault("case_sensitive", Value.FALSE).getBoolean(); + super.configure(config, host); if (config.containsKey("options")) { Value optionsValue = config.get("options"); if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom sting type requires options passed as a list"); validOptions = ((ListValue) optionsValue).getItems().stream() - .map(v -> caseSensitive?v.getString():v.getString().toLowerCase(Locale.ROOT)) + .map(v -> caseSensitive?v.getString():(v.getString().toLowerCase(Locale.ROOT))) .collect(Collectors.toSet()); } } @Override - protected Collection getOptions() { return validOptions.isEmpty()?super.getOptions():validOptions; } + protected Collection getOptions(CommandContext context, CarpetScriptHost host) throws CommandSyntaxException + { + return validOptions.isEmpty()?super.getOptions(context, host):validOptions; + } @Override - protected Supplier builder() { return WordArgument::new; } + protected Supplier factory() { return WordArgument::new; } } private static class WordArgument extends StringArgument @@ -404,7 +451,7 @@ public abstract class CommandArgument @Override public ArgumentType getArgumentType() { return StringArgumentType.word(); } @Override - protected Supplier builder() { return WordArgument::new; } + protected Supplier factory() { return WordArgument::new; } } private static class GreedyStringArgument extends StringArgument @@ -413,7 +460,7 @@ public abstract class CommandArgument @Override public ArgumentType getArgumentType() { return StringArgumentType.greedyString(); } @Override - protected Supplier builder() { return GreedyStringArgument::new; } + protected Supplier factory() { return GreedyStringArgument::new; } } private static class BlockPosArgument extends CommandArgument @@ -441,14 +488,14 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); mustBeLoaded = config.getOrDefault("loaded", Value.FALSE).getBoolean(); } @Override - protected Supplier builder() + protected Supplier factory() { return BlockPosArgument::new; } @@ -476,14 +523,14 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); blockCentered = config.getOrDefault("block_centered", Value.TRUE).getBoolean(); } @Override - protected Supplier builder() + protected Supplier factory() { return LocationArgument::new; } @@ -496,7 +543,7 @@ public abstract class CommandArgument private EntityArgument() { - super("entity", EntityArgumentType.entities().getExamples(), false); + super("entities", EntityArgumentType.entities().getExamples(), false); onlyFans = false; single = false; } @@ -524,15 +571,15 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); onlyFans = config.getOrDefault("players", Value.FALSE).getBoolean(); single = config.getOrDefault("single", Value.FALSE).getBoolean(); } @Override - protected Supplier builder() + protected Supplier factory() { return EntityArgument::new; } @@ -544,7 +591,7 @@ public abstract class CommandArgument private PlayerProfileArgument() { - super("player", GameProfileArgumentType.gameProfile().getExamples(), false); + super("players", GameProfileArgumentType.gameProfile().getExamples(), false); single = false; } @Override @@ -565,14 +612,14 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); single = config.getOrDefault("single", Value.FALSE).getBoolean(); } @Override - protected Supplier builder() + protected Supplier factory() { return PlayerProfileArgument::new; } @@ -606,14 +653,14 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); single = config.getOrDefault("single", Value.FALSE).getBoolean(); } @Override - protected Supplier builder() + protected Supplier factory() { return PlayerProfileArgument::new; } @@ -643,14 +690,14 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); mapRequired = !config.getOrDefault("allow_element", Value.FALSE).getBoolean(); } @Override - protected Supplier builder() + protected Supplier factory() { return TagArgument::new; } @@ -683,15 +730,15 @@ public abstract class CommandArgument } @Override - protected Supplier builder() + protected Supplier factory() { return CustomIdentifierArgument::new; } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); if (config.containsKey("options")) { Value optionsValue = config.get("options"); @@ -731,9 +778,9 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); if (config.containsKey("min")) { min = NumericValue.asNumber(config.get("min"), "min").getDouble(); @@ -746,7 +793,7 @@ public abstract class CommandArgument } @Override - protected Supplier builder() + protected Supplier factory() { return FloatArgument::new; } @@ -782,9 +829,9 @@ public abstract class CommandArgument } @Override - protected void configure(Map config) + protected void configure(Map config, CarpetScriptHost host) { - super.configure(config); + super.configure(config, host); if (config.containsKey("min")) { min = NumericValue.asNumber(config.get("min"), "min").getLong(); @@ -797,9 +844,9 @@ public abstract class CommandArgument } @Override - protected Supplier builder() + protected Supplier factory() { - return FloatArgument::new; + return IntArgument::new; } } @@ -867,7 +914,7 @@ public abstract class CommandArgument } @Override - protected Supplier builder() + protected Supplier factory() { return () -> new VanillaUnconfigurableArgument(getTypeSuffix(), argumentTypeSupplier, valueExtractor, providesExamples); } diff --git a/src/main/java/carpet/script/command/CommandToken.java b/src/main/java/carpet/script/command/CommandToken.java new file mode 100644 index 00000000..07953b72 --- /dev/null +++ b/src/main/java/carpet/script/command/CommandToken.java @@ -0,0 +1,97 @@ +package carpet.script.command; + +import carpet.script.CarpetScriptHost; +import carpet.script.value.FunctionValue; +import com.google.common.collect.Lists; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.server.command.ServerCommandSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static net.minecraft.server.command.CommandManager.literal; + +public class CommandToken implements Comparable +{ + public String surface; + public boolean isArgument; + public CommandArgument type; + + private CommandToken(String surface, CommandArgument type ) + { + this.surface = surface; + this.type = type; + isArgument = type != null; + } + + public static CommandToken getToken(String source, CarpetScriptHost host) + { + // todo add more type checking and return null + if (!source.startsWith("<")) + { + if (!source.matches("[_a-zA-Z]+")) return null; + return new CommandToken(source, null); + } + source = source.substring(1, source.length()-1); + if (!source.matches("[_a-zA-Z]+")) return null; + CommandArgument arg = CommandArgument.getTypeForArgument(source, host); + return new CommandToken(source, arg); + } + + public static List parseSpec(String spec, CarpetScriptHost host) throws CommandSyntaxException + { + spec = spec.trim(); + if (spec.isEmpty()) return Collections.emptyList(); + List elements = new ArrayList<>(); + for (String el: spec.split("\\s+")) + { + CommandToken tok = CommandToken.getToken(el, host); + if (tok == null) throw CommandArgument.error("Unrecognized command token: "+ el); + elements.add(tok); + } + return elements; + } + + public static String specFromSignature(FunctionValue function) + { + List tokens = Lists.newArrayList(function.getString()); + for (String arg : function.getArguments()) tokens.add("<"+arg+">"); + return String.join(" ", tokens); + } + + + public ArgumentBuilder getCommandNode(CarpetScriptHost host) throws CommandSyntaxException + { + if (isArgument) + return CommandArgument.argumentNode(surface, host); + return literal(surface); + + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CommandToken that = (CommandToken) o; + return surface.equals(that.surface) && + Objects.equals(type, that.type); + } + + @Override + public int hashCode() + { + return Objects.hash(surface, type); + } + + @Override + public int compareTo(CommandToken o) + { + if (isArgument && !o.isArgument) return 1; + if (!isArgument && o.isArgument) return -1; + return surface.compareTo(o.surface); + } +}