Merge branch 'master' into 1.17
This commit is contained in:
commit
c8b5dbb26d
9 files changed
+897
-177
No files matched your search
+273
-26
@@ -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.
|
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 <player> run script in <app> 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 <app> 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 `/<app>` command, see below
|
||||||
|
|
||||||
|
## Custom app commands
|
||||||
|
|
||||||
|
Apps can register custom commands added to the existing command system under `/<app>` where `<app>` 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
|
||||||
|
`/<app> <fun> <args...>`. 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 `/<app>` 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 <entities>' -> 'clear_all',
|
||||||
|
'clear <entities> <effect>' -> 'clear',
|
||||||
|
'give <entities> <effect>' -> ['apply', -1, 0, false, true],
|
||||||
|
'give <entities> <effect> <seconds>' -> ['apply', 0, false, true],
|
||||||
|
'give <entities> <effect> <seconds> <amplifier>' -> ['apply', false, true],
|
||||||
|
'give <entities> <effect> <seconds> <amplifier> <hideParticles>' -> ['apply', true],
|
||||||
|
'give <entities> <effect> <seconds> <amplifier> <hideParticles> <showIcon>' -> '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
|
## Dimension warning
|
||||||
|
|
||||||
One note, which is important is that most of the calls for entities and blocks would refer to the current
|
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,
|
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
|
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,
|
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('*')`,
|
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')))
|
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?)`
|
### `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
|
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
|
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
|
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
|
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.
|
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
|
Unloading the app will only mask their command tree, not remove it. This has the same effect than not having that command
|
||||||
saves its global state. If more cleanup is needed, one can define an `__on_close()` function which will be
|
at all, with the exception that if you load a diffrent app with the same name, this may cause commands to reappear.
|
||||||
executed when the module is unloaded, or server is closing, or crashing. However, there is no need to do that
|
To remove the commands fully, use `/reload`.
|
||||||
explicitly for the things mentioned in the previous statement.
|
|
||||||
|
|
||||||
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 <app> ...`
|
### `/script in <app> ...`
|
||||||
|
|
||||||
|
|||||||
@@ -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')))
|
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?)`
|
### `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
|
Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the
|
||||||
|
|||||||
@@ -2,11 +2,251 @@
|
|||||||
|
|
||||||
Here is the gist of the Minecraft related functions. Otherwise the CarpetScript could live without Minecraft.
|
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 <player> run script in <app> 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 <app> 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 `/<app>` command, see below
|
||||||
|
|
||||||
|
## Custom app commands
|
||||||
|
|
||||||
|
Apps can register custom commands added to the existing command system under `/<app>` where `<app>` 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
|
||||||
|
`/<app> <fun> <args...>`. 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 `/<app>` 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 <entities>' -> 'clear_all',
|
||||||
|
'clear <entities> <effect>' -> 'clear',
|
||||||
|
'give <entities> <effect>' -> ['apply', -1, 0, false, true],
|
||||||
|
'give <entities> <effect> <seconds>' -> ['apply', 0, false, true],
|
||||||
|
'give <entities> <effect> <seconds> <amplifier>' -> ['apply', false, true],
|
||||||
|
'give <entities> <effect> <seconds> <amplifier> <hideParticles>' -> ['apply', true],
|
||||||
|
'give <entities> <effect> <seconds> <amplifier> <hideParticles> <showIcon>' -> '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
|
## Dimension warning
|
||||||
|
|
||||||
One note, which is important is that most of the calls for entities and blocks would refer to the current
|
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,
|
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
|
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,
|
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('*')`,
|
using `/execute in overworld/the_nether/the_end run script run ...` and query players using `player('*')`,
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
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.
|
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
|
Unloading the app will only mask their command tree, not remove it. This has the same effect than not having that command
|
||||||
saves its global state. If more cleanup is needed, one can define an `__on_close()` function which will be
|
at all, with the exception that if you load a diffrent app with the same name, this may cause commands to reappear.
|
||||||
executed when the module is unloaded, or server is closing, or crashing. However, there is no need to do that
|
To remove the commands fully, use `/reload`.
|
||||||
explicitly for the things mentioned in the previous statement.
|
|
||||||
|
|
||||||
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 <app> ...`
|
### `/script in <app> ...`
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import carpet.script.api.Auxiliary;
|
|||||||
import carpet.script.argument.FunctionArgument;
|
import carpet.script.argument.FunctionArgument;
|
||||||
import carpet.script.bundled.Module;
|
import carpet.script.bundled.Module;
|
||||||
import carpet.script.command.CommandArgument;
|
import carpet.script.command.CommandArgument;
|
||||||
|
import carpet.script.command.CommandToken;
|
||||||
import carpet.script.exception.CarpetExpressionException;
|
import carpet.script.exception.CarpetExpressionException;
|
||||||
import carpet.script.exception.ExpressionException;
|
import carpet.script.exception.ExpressionException;
|
||||||
import carpet.script.exception.InternalExpressionException;
|
import carpet.script.exception.InternalExpressionException;
|
||||||
@@ -16,24 +17,32 @@ import carpet.script.value.StringValue;
|
|||||||
import carpet.script.value.Value;
|
import carpet.script.value.Value;
|
||||||
import carpet.CarpetSettings;
|
import carpet.CarpetSettings;
|
||||||
import carpet.utils.Messenger;
|
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.CommandSyntaxException;
|
||||||
|
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||||
import net.minecraft.entity.Entity;
|
import net.minecraft.entity.Entity;
|
||||||
import net.minecraft.entity.EntityType;
|
import net.minecraft.entity.EntityType;
|
||||||
import net.minecraft.nbt.Tag;
|
import net.minecraft.nbt.Tag;
|
||||||
import net.minecraft.server.command.ServerCommandSource;
|
import net.minecraft.server.command.ServerCommandSource;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
import net.minecraft.server.world.ServerWorld;
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
import net.minecraft.text.LiteralText;
|
||||||
import net.minecraft.util.math.BlockPos;
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
|
import static net.minecraft.server.command.CommandManager.literal;
|
||||||
|
|
||||||
public class CarpetScriptHost extends ScriptHost
|
public class CarpetScriptHost extends ScriptHost
|
||||||
{
|
{
|
||||||
@@ -100,6 +109,62 @@ public class CarpetScriptHost extends ScriptHost
|
|||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int execute(CommandContext<ServerCommandSource> ctx, String hostName, FunctionArgument<Value> funcSpec, List<String> paramNames) throws CommandSyntaxException
|
||||||
|
{
|
||||||
|
CarpetScriptHost cHost = CarpetServer.scriptServer.modules.get(hostName).retrieveForExecution(ctx.getSource());
|
||||||
|
List<String> 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<Value> 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<ServerCommandSource> addPathToCommand(
|
||||||
|
LiteralArgumentBuilder<ServerCommandSource> command,
|
||||||
|
List<CommandToken> path,
|
||||||
|
FunctionArgument<Value> functionSpec
|
||||||
|
) throws CommandSyntaxException
|
||||||
|
{
|
||||||
|
String hostName = main.getName();
|
||||||
|
List<String> 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<CommandToken> reversedPath = new ArrayList<>(path);
|
||||||
|
Collections.reverse(reversedPath);
|
||||||
|
ArgumentBuilder<ServerCommandSource, ?> 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<ServerCommandSource> getNewCommandTree(
|
||||||
|
List<Pair<List<CommandToken>,FunctionArgument<Value>>> entries
|
||||||
|
) throws CommandSyntaxException
|
||||||
|
{
|
||||||
|
String hostName = main.getName();
|
||||||
|
LiteralArgumentBuilder<ServerCommandSource> command = literal(hostName).
|
||||||
|
requires((player) -> CarpetServer.scriptServer.modules.containsKey(hostName));
|
||||||
|
for (Pair<List<CommandToken>,FunctionArgument<Value>> commandData : entries)
|
||||||
|
{
|
||||||
|
command = this.addPathToCommand(command, commandData.getKey(), commandData.getValue());
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ScriptHost duplicate()
|
protected ScriptHost duplicate()
|
||||||
{
|
{
|
||||||
@@ -150,7 +215,7 @@ public class CarpetScriptHost extends ScriptHost
|
|||||||
Value spec = typeData.getValue();
|
Value spec = typeData.getValue();
|
||||||
if (!(spec instanceof MapValue)) throw new InternalExpressionException("Spec for '"+argument+"' should be a map");
|
if (!(spec instanceof MapValue)) throw new InternalExpressionException("Spec for '"+argument+"' should be a map");
|
||||||
Map<String, Value> specData = ((MapValue) spec).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue));
|
Map<String, Value> 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;
|
appConfig = config;
|
||||||
@@ -162,24 +227,59 @@ public class CarpetScriptHost extends ScriptHost
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean readCommands()
|
static class ListComparator<T extends Comparable<T>> implements Comparator<Pair<List<T>,?>>
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public int compare(Pair<List<T>,?> p1, Pair<List<T>,?> p2) {
|
||||||
|
List<T> o1 = p1.getKey();
|
||||||
|
List<T> 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<ServerCommandSource> readCommands() throws CommandSyntaxException
|
||||||
{
|
{
|
||||||
Value commands = appConfig.get(StringValue.of("commands"));
|
Value commands = appConfig.get(StringValue.of("commands"));
|
||||||
if (commands == null) return true;
|
|
||||||
|
if (commands == null) return null;
|
||||||
if (!(commands instanceof MapValue))
|
if (!(commands instanceof MapValue))
|
||||||
throw new InternalExpressionException("'commands' element in config should be a map");
|
throw CommandArgument.error("'commands' element in config should be a map");
|
||||||
for (Map.Entry<Value, Value> commandsData : ((MapValue)commands).getMap().entrySet())
|
List<Pair<List<CommandToken>,FunctionArgument<Value>>> commandEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Map.Entry<Value, Value> commandsData : ((MapValue)commands).getMap().entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList()))
|
||||||
{
|
{
|
||||||
String path = commandsData.getKey().getString();
|
List<CommandToken> elements = CommandToken.parseSpec(commandsData.getKey().getString(), this);
|
||||||
FunctionArgument<Value> funSpec = FunctionArgument.fromCommandSpec(this, commandsData.getValue());
|
FunctionArgument<Value> funSpec = FunctionArgument.fromCommandSpec(this, commandsData.getValue());
|
||||||
//if (fun)
|
commandEntries.add(Pair.of(elements, funSpec));
|
||||||
|
|
||||||
//... WIP
|
|
||||||
|
|
||||||
//Map<String, Value> specData = ((MapValue) spec).getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue));
|
|
||||||
//appArgTypes.put(argument, CommandArgument.buildFromConfig(argument, specData));
|
|
||||||
}
|
}
|
||||||
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<CommandToken> first = commandEntries.get(i).getKey();
|
||||||
|
List<CommandToken> 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
|
@Override
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package carpet.script;
|
package carpet.script;
|
||||||
|
|
||||||
|
import carpet.script.argument.FunctionArgument;
|
||||||
import carpet.script.bundled.BundledModule;
|
import carpet.script.bundled.BundledModule;
|
||||||
import carpet.CarpetSettings;
|
import carpet.CarpetSettings;
|
||||||
import carpet.CarpetServer;
|
import carpet.CarpetServer;
|
||||||
import carpet.script.bundled.FileModule;
|
import carpet.script.bundled.FileModule;
|
||||||
import carpet.script.bundled.Module;
|
import carpet.script.bundled.Module;
|
||||||
import carpet.script.command.CommandArgument;
|
import carpet.script.command.CommandToken;
|
||||||
import carpet.script.exception.InvalidCallbackException;
|
import carpet.script.exception.InvalidCallbackException;
|
||||||
import carpet.script.value.FunctionValue;
|
import carpet.script.value.FunctionValue;
|
||||||
import carpet.script.value.StringValue;
|
import carpet.script.value.StringValue;
|
||||||
@@ -13,21 +14,16 @@ import carpet.script.value.Value;
|
|||||||
import carpet.utils.Messenger;
|
import carpet.utils.Messenger;
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
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.CommandSyntaxException;
|
||||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
|
||||||
import com.mojang.brigadier.tree.CommandNode;
|
import com.mojang.brigadier.tree.CommandNode;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.command.ServerCommandSource;
|
import net.minecraft.server.command.ServerCommandSource;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
import net.minecraft.text.LiteralText;
|
|
||||||
import net.minecraft.util.WorldSavePath;
|
import net.minecraft.util.WorldSavePath;
|
||||||
import net.minecraft.util.math.BlockPos;
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -240,7 +236,31 @@ public class CarpetScriptServer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//addEvents(source, name);
|
//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<ServerCommandSource> 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);
|
removeScriptHost(source, name, false, false);
|
||||||
Messenger.m(source, "r Failed to build command system for "+name+" thus failed to load the app");
|
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()))
|
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
|
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)
|
catch (CommandSyntaxException e)
|
||||||
{
|
{
|
||||||
@@ -320,49 +345,6 @@ public class CarpetScriptServer
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int execute(CommandContext<ServerCommandSource> ctx, String hostName, FunctionValue function, List<String> paramNames) throws CommandSyntaxException
|
|
||||||
{
|
|
||||||
CarpetScriptHost cHost = modules.get(hostName).retrieveForExecution(ctx.getSource());
|
|
||||||
List<String> 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<Value> 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<ServerCommandSource> getFancyCommand(LiteralArgumentBuilder<ServerCommandSource> command, CarpetScriptHost host, FunctionValue function ) throws CommandSyntaxException
|
|
||||||
{
|
|
||||||
List<String> 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<String> reversedArgs = new ArrayList<>(argNames);
|
|
||||||
Collections.reverse(reversedArgs);
|
|
||||||
RequiredArgumentBuilder<ServerCommandSource, ?> 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)
|
public boolean removeScriptHost(ServerCommandSource source, String name, boolean notifySource, boolean isRuleApp)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public class FunctionArgument<T> extends Argument
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
String name = funSpec.getString();
|
String name = first.getString();
|
||||||
function = host.getAssertFunction(host.main, name);
|
function = host.getAssertFunction(host.main, name);
|
||||||
}
|
}
|
||||||
if (params.size() > 1) args = params.subList(1,params.size());
|
if (params.size() > 1) args = params.subList(1,params.size());
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package carpet.script.command;
|
|||||||
|
|
||||||
import carpet.fakes.BlockStateArgumentInterface;
|
import carpet.fakes.BlockStateArgumentInterface;
|
||||||
import carpet.script.CarpetScriptHost;
|
import carpet.script.CarpetScriptHost;
|
||||||
|
import carpet.script.argument.FunctionArgument;
|
||||||
import carpet.script.exception.InternalExpressionException;
|
import carpet.script.exception.InternalExpressionException;
|
||||||
import carpet.script.value.BlockValue;
|
import carpet.script.value.BlockValue;
|
||||||
import carpet.script.value.EntityValue;
|
import carpet.script.value.EntityValue;
|
||||||
import carpet.script.value.FormattedTextValue;
|
import carpet.script.value.FormattedTextValue;
|
||||||
import carpet.script.value.ListValue;
|
import carpet.script.value.ListValue;
|
||||||
|
import carpet.script.value.MapValue;
|
||||||
import carpet.script.value.NBTSerializableValue;
|
import carpet.script.value.NBTSerializableValue;
|
||||||
import carpet.script.value.NumericValue;
|
import carpet.script.value.NumericValue;
|
||||||
import carpet.script.value.StringValue;
|
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.arguments.StringArgumentType;
|
||||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
import com.mojang.brigadier.context.CommandContext;
|
import com.mojang.brigadier.context.CommandContext;
|
||||||
|
import com.mojang.brigadier.context.ParsedCommandNode;
|
||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||||
import com.mojang.brigadier.suggestion.Suggestions;
|
import com.mojang.brigadier.suggestion.Suggestions;
|
||||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
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.advancement.Advancement;
|
||||||
import net.minecraft.command.CommandSource;
|
import net.minecraft.command.CommandSource;
|
||||||
import net.minecraft.command.argument.AngleArgumentType;
|
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.math.Vec2f;
|
||||||
import net.minecraft.util.registry.Registry;
|
import net.minecraft.util.registry.Registry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -89,6 +96,11 @@ import static net.minecraft.server.command.CommandManager.argument;
|
|||||||
|
|
||||||
public abstract class CommandArgument
|
public abstract class CommandArgument
|
||||||
{
|
{
|
||||||
|
public static CommandSyntaxException error(String text)
|
||||||
|
{
|
||||||
|
return new SimpleCommandExceptionType(new LiteralText(text)).create();
|
||||||
|
}
|
||||||
|
|
||||||
private static final List<? extends CommandArgument> baseTypes = Lists.newArrayList(
|
private static final List<? extends CommandArgument> baseTypes = Lists.newArrayList(
|
||||||
// default
|
// default
|
||||||
new StringArgument(),
|
new StringArgument(),
|
||||||
@@ -241,10 +253,10 @@ public abstract class CommandArgument
|
|||||||
|
|
||||||
public static final CommandArgument DEFAULT = baseTypes.get(0);
|
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[] components = argument.split("_");
|
||||||
String suffix = components[components.length-1].toLowerCase(Locale.ROOT);
|
String suffix = components[components.length-1];
|
||||||
CommandArgument arg = host.appArgTypes.get(suffix);
|
CommandArgument arg = host.appArgTypes.get(suffix);
|
||||||
if (arg != null) return arg;
|
if (arg != null) return arg;
|
||||||
return builtIns.getOrDefault(suffix, DEFAULT);
|
return builtIns.getOrDefault(suffix, DEFAULT);
|
||||||
@@ -254,13 +266,16 @@ public abstract class CommandArgument
|
|||||||
{
|
{
|
||||||
CommandArgument arg = getTypeForArgument(param, host);
|
CommandArgument arg = getTypeForArgument(param, host);
|
||||||
if (arg.suggestionProvider != null) return argument(param, arg.getArgumentType()).suggests(arg.suggestionProvider);
|
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 String suffix;
|
||||||
protected Collection<String> examples;
|
protected Collection<String> examples;
|
||||||
protected boolean needsMatching = false;
|
protected boolean needsMatching;
|
||||||
|
protected boolean caseSensitive = true;
|
||||||
protected SuggestionProvider<ServerCommandSource> suggestionProvider;
|
protected SuggestionProvider<ServerCommandSource> suggestionProvider;
|
||||||
|
protected FunctionArgument<Value> customSuggester;
|
||||||
|
|
||||||
|
|
||||||
protected CommandArgument(
|
protected CommandArgument(
|
||||||
String suffix,
|
String suffix,
|
||||||
@@ -287,23 +302,29 @@ public abstract class CommandArgument
|
|||||||
return suffix;
|
return suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CommandArgument buildFromConfig(String suffix, Map<String, Value> config)
|
public static CommandArgument buildFromConfig(String suffix, Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
if (!config.containsKey("type"))
|
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();
|
String baseType = config.get("type").getString();
|
||||||
if (!builtIns.containsKey(baseType))
|
if (!builtIns.containsKey(baseType))
|
||||||
throw new InternalExpressionException("Unknown base type: "+baseType);
|
throw new InternalExpressionException("Unknown base type: "+baseType);
|
||||||
CommandArgument variant = builtIns.get(baseType).builder().get();
|
CommandArgument variant = builtIns.get(baseType).factory().get();
|
||||||
variant.configure(config);
|
variant.configure(config, host);
|
||||||
variant.suffix = suffix;
|
variant.suffix = suffix;
|
||||||
return variant;
|
return variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> 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("suggest"))
|
||||||
{
|
{
|
||||||
|
if (config.containsKey("suggester")) throw new InternalExpressionException("Attempted to provide 'suggest' list while 'suggester' is present");
|
||||||
Value suggestionValue = config.get("suggest");
|
Value suggestionValue = config.get("suggest");
|
||||||
if (!(suggestionValue instanceof ListValue)) throw new InternalExpressionException("Argument suggestions needs to be a list");
|
if (!(suggestionValue instanceof ListValue)) throw new InternalExpressionException("Argument suggestions needs to be a list");
|
||||||
examples = ((ListValue) suggestionValue).getItems().stream()
|
examples = ((ListValue) suggestionValue).getItems().stream()
|
||||||
@@ -311,34 +332,59 @@ public abstract class CommandArgument
|
|||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
if (!examples.isEmpty()) needsMatching = true;
|
if (!examples.isEmpty()) needsMatching = true;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
public CompletableFuture<Suggestions> suggest(
|
public CompletableFuture<Suggestions> suggest(
|
||||||
CommandContext<ServerCommandSource> context,
|
CommandContext<ServerCommandSource> context,
|
||||||
SuggestionsBuilder suggestionsBuilder
|
SuggestionsBuilder suggestionsBuilder,
|
||||||
)
|
CarpetScriptHost host
|
||||||
|
) throws CommandSyntaxException
|
||||||
{
|
{
|
||||||
String prefix = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT);
|
String prefix = suggestionsBuilder.getRemaining();
|
||||||
suggestFor(prefix).forEach(suggestionsBuilder::suggest);
|
if (!caseSensitive) prefix = prefix.toLowerCase(Locale.ROOT);
|
||||||
|
suggestFor(context, prefix, host).forEach(suggestionsBuilder::suggest);
|
||||||
return suggestionsBuilder.buildFuture();
|
return suggestionsBuilder.buildFuture();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<String> suggestFor(String prefix)
|
protected List<String> suggestFor(CommandContext<ServerCommandSource> 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<String> getOptions()
|
protected Collection<String> getOptions(CommandContext<ServerCommandSource> context, CarpetScriptHost host) throws CommandSyntaxException
|
||||||
{
|
{
|
||||||
|
if (customSuggester != null)
|
||||||
|
{
|
||||||
|
Map<Value, Value> params = new HashMap<>();
|
||||||
|
for(ParsedCommandNode<ServerCommandSource> pnode : context.getNodes())
|
||||||
|
{
|
||||||
|
CommandNode<ServerCommandSource> node = pnode.getNode();
|
||||||
|
if (node instanceof ArgumentCommandNode)
|
||||||
|
{
|
||||||
|
params.put(StringValue.of(node.getName()), CommandArgument.getValue(context, node.getName(), host));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Value> 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("");
|
//return Lists.newArrayList("");
|
||||||
// better than nothing I guess
|
// better than nothing I guess
|
||||||
// nothing is such a bad default.
|
// nothing is such a bad default.
|
||||||
if (needsMatching) return examples;
|
|
||||||
return Collections.singletonList("... "+getTypeSuffix());
|
return Collections.singletonList("... "+getTypeSuffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean optionMatchesPrefix(String prefix, String option)
|
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)
|
for(int i = 0; !option.startsWith(prefix, i); ++i)
|
||||||
{
|
{
|
||||||
i = option.indexOf('_', i);
|
i = option.indexOf('_', i);
|
||||||
@@ -347,12 +393,11 @@ public abstract class CommandArgument
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Supplier<CommandArgument> builder();
|
protected abstract Supplier<CommandArgument> factory();
|
||||||
|
|
||||||
private static class StringArgument extends CommandArgument
|
private static class StringArgument extends CommandArgument
|
||||||
{
|
{
|
||||||
Set<String> validOptions = Collections.emptySet();
|
Set<String> validOptions = Collections.emptySet();
|
||||||
boolean caseSensitive = false;
|
|
||||||
private StringArgument()
|
private StringArgument()
|
||||||
{
|
{
|
||||||
super("string", StringArgumentType.StringType.QUOTABLE_PHRASE.getExamples(), true);
|
super("string", StringArgumentType.StringType.QUOTABLE_PHRASE.getExamples(), true);
|
||||||
@@ -377,25 +422,27 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
caseSensitive = config.getOrDefault("case_sensitive", Value.FALSE).getBoolean();
|
|
||||||
if (config.containsKey("options"))
|
if (config.containsKey("options"))
|
||||||
{
|
{
|
||||||
Value optionsValue = config.get("options");
|
Value optionsValue = config.get("options");
|
||||||
if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom sting type requires options passed as a list");
|
if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom sting type requires options passed as a list");
|
||||||
validOptions = ((ListValue) optionsValue).getItems().stream()
|
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());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Collection<String> getOptions() { return validOptions.isEmpty()?super.getOptions():validOptions; }
|
protected Collection<String> getOptions(CommandContext<ServerCommandSource> context, CarpetScriptHost host) throws CommandSyntaxException
|
||||||
|
{
|
||||||
|
return validOptions.isEmpty()?super.getOptions(context, host):validOptions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder() { return WordArgument::new; }
|
protected Supplier<CommandArgument> factory() { return WordArgument::new; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WordArgument extends StringArgument
|
private static class WordArgument extends StringArgument
|
||||||
@@ -404,7 +451,7 @@ public abstract class CommandArgument
|
|||||||
@Override
|
@Override
|
||||||
public ArgumentType<?> getArgumentType() { return StringArgumentType.word(); }
|
public ArgumentType<?> getArgumentType() { return StringArgumentType.word(); }
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder() { return WordArgument::new; }
|
protected Supplier<CommandArgument> factory() { return WordArgument::new; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class GreedyStringArgument extends StringArgument
|
private static class GreedyStringArgument extends StringArgument
|
||||||
@@ -413,7 +460,7 @@ public abstract class CommandArgument
|
|||||||
@Override
|
@Override
|
||||||
public ArgumentType<?> getArgumentType() { return StringArgumentType.greedyString(); }
|
public ArgumentType<?> getArgumentType() { return StringArgumentType.greedyString(); }
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder() { return GreedyStringArgument::new; }
|
protected Supplier<CommandArgument> factory() { return GreedyStringArgument::new; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class BlockPosArgument extends CommandArgument
|
private static class BlockPosArgument extends CommandArgument
|
||||||
@@ -441,14 +488,14 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
mustBeLoaded = config.getOrDefault("loaded", Value.FALSE).getBoolean();
|
mustBeLoaded = config.getOrDefault("loaded", Value.FALSE).getBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return BlockPosArgument::new;
|
return BlockPosArgument::new;
|
||||||
}
|
}
|
||||||
@@ -476,14 +523,14 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
blockCentered = config.getOrDefault("block_centered", Value.TRUE).getBoolean();
|
blockCentered = config.getOrDefault("block_centered", Value.TRUE).getBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return LocationArgument::new;
|
return LocationArgument::new;
|
||||||
}
|
}
|
||||||
@@ -496,7 +543,7 @@ public abstract class CommandArgument
|
|||||||
|
|
||||||
private EntityArgument()
|
private EntityArgument()
|
||||||
{
|
{
|
||||||
super("entity", EntityArgumentType.entities().getExamples(), false);
|
super("entities", EntityArgumentType.entities().getExamples(), false);
|
||||||
onlyFans = false;
|
onlyFans = false;
|
||||||
single = false;
|
single = false;
|
||||||
}
|
}
|
||||||
@@ -524,15 +571,15 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
onlyFans = config.getOrDefault("players", Value.FALSE).getBoolean();
|
onlyFans = config.getOrDefault("players", Value.FALSE).getBoolean();
|
||||||
single = config.getOrDefault("single", Value.FALSE).getBoolean();
|
single = config.getOrDefault("single", Value.FALSE).getBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return EntityArgument::new;
|
return EntityArgument::new;
|
||||||
}
|
}
|
||||||
@@ -544,7 +591,7 @@ public abstract class CommandArgument
|
|||||||
|
|
||||||
private PlayerProfileArgument()
|
private PlayerProfileArgument()
|
||||||
{
|
{
|
||||||
super("player", GameProfileArgumentType.gameProfile().getExamples(), false);
|
super("players", GameProfileArgumentType.gameProfile().getExamples(), false);
|
||||||
single = false;
|
single = false;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
@@ -565,14 +612,14 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
single = config.getOrDefault("single", Value.FALSE).getBoolean();
|
single = config.getOrDefault("single", Value.FALSE).getBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return PlayerProfileArgument::new;
|
return PlayerProfileArgument::new;
|
||||||
}
|
}
|
||||||
@@ -606,14 +653,14 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
single = config.getOrDefault("single", Value.FALSE).getBoolean();
|
single = config.getOrDefault("single", Value.FALSE).getBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return PlayerProfileArgument::new;
|
return PlayerProfileArgument::new;
|
||||||
}
|
}
|
||||||
@@ -643,14 +690,14 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
mapRequired = !config.getOrDefault("allow_element", Value.FALSE).getBoolean();
|
mapRequired = !config.getOrDefault("allow_element", Value.FALSE).getBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return TagArgument::new;
|
return TagArgument::new;
|
||||||
}
|
}
|
||||||
@@ -683,15 +730,15 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return CustomIdentifierArgument::new;
|
return CustomIdentifierArgument::new;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
if (config.containsKey("options"))
|
if (config.containsKey("options"))
|
||||||
{
|
{
|
||||||
Value optionsValue = config.get("options");
|
Value optionsValue = config.get("options");
|
||||||
@@ -731,9 +778,9 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
if (config.containsKey("min"))
|
if (config.containsKey("min"))
|
||||||
{
|
{
|
||||||
min = NumericValue.asNumber(config.get("min"), "min").getDouble();
|
min = NumericValue.asNumber(config.get("min"), "min").getDouble();
|
||||||
@@ -746,7 +793,7 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return FloatArgument::new;
|
return FloatArgument::new;
|
||||||
}
|
}
|
||||||
@@ -782,9 +829,9 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(Map<String, Value> config)
|
protected void configure(Map<String, Value> config, CarpetScriptHost host)
|
||||||
{
|
{
|
||||||
super.configure(config);
|
super.configure(config, host);
|
||||||
if (config.containsKey("min"))
|
if (config.containsKey("min"))
|
||||||
{
|
{
|
||||||
min = NumericValue.asNumber(config.get("min"), "min").getLong();
|
min = NumericValue.asNumber(config.get("min"), "min").getLong();
|
||||||
@@ -797,9 +844,9 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return FloatArgument::new;
|
return IntArgument::new;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,7 +914,7 @@ public abstract class CommandArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Supplier<CommandArgument> builder()
|
protected Supplier<CommandArgument> factory()
|
||||||
{
|
{
|
||||||
return () -> new VanillaUnconfigurableArgument(getTypeSuffix(), argumentTypeSupplier, valueExtractor, providesExamples);
|
return () -> new VanillaUnconfigurableArgument(getTypeSuffix(), argumentTypeSupplier, valueExtractor, providesExamples);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<CommandToken>
|
||||||
|
{
|
||||||
|
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<CommandToken> parseSpec(String spec, CarpetScriptHost host) throws CommandSyntaxException
|
||||||
|
{
|
||||||
|
spec = spec.trim();
|
||||||
|
if (spec.isEmpty()) return Collections.emptyList();
|
||||||
|
List<CommandToken> 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<String> tokens = Lists.newArrayList(function.getString());
|
||||||
|
for (String arg : function.getArguments()) tokens.add("<"+arg+">");
|
||||||
|
return String.join(" ", tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ArgumentBuilder<ServerCommandSource, ?> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user