diff --git a/.gitignore b/.gitignore index aca7bbba..3e26bc92 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ out/ classes/ publish/ +# git + +.shelf/ + # idea .idea/ @@ -27,4 +31,5 @@ bin/ # fabric -run/ \ No newline at end of file +run/ +logs/ \ No newline at end of file diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md index 9ff30fca..f1823eb3 100644 --- a/docs/scarpet/Full.md +++ b/docs/scarpet/Full.md @@ -151,9 +151,10 @@ have memory and act like objects so to speak. Check `outer(var)` for details. ## Code delivery, line indicators Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing -your code in apps (files with `.sc` extension that can be placed inside "/scripts" folder in the world files and -loaded as a scarpet app with command `/script load [app_name]`. Scarpet apps loaded from disk should only -contain code, no need to start with "/script run" prefix +your code in apps (files with `.sc` extension that can be placed inside `/scripts` folder in the world files +or as a globally available app in singleplayer in the `.minecraft/config/carpet/scripts` folder and loaded +as a Scarpet app with the command `/script load [app_name]`. Scarpet apps loaded from disk should only +contain code, no need to start with `/script run` prefix. The following is the code that could be provided in a `foo.sc` app file located in world `/scripts` folder @@ -803,6 +804,9 @@ a = 3; task_thread('temp', _(outer(a), b) -> foo(a,b), 5) => Another example of running the same thing passing arguments using closure over anonymous function as well as passing a parameter. +In case you want to create a task based on a function that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + ### `sleep()` `sleep(timeout)`, `sleep(timeout, close_expr)` @@ -1372,7 +1376,7 @@ foo(... x) -> ... # all arguments for foo are included in the list ### `import(module_name, symbols ...)` Imports symbols from other apps and libraries into the current one: global variables or functions, allowing to use -them in the current app. This include other symbols imported by these modules. Scarpet supports cicular dependencies, +them in the current app. This includes other symbols imported by these modules. Scarpet supports circular dependencies, but if symbols are used directly in the module body rather than functions, it may not be able to retrieve them. Returns full list of available symbols that could be imported from this module, which can be used to debug import issues, and list contents of libraries. @@ -1381,9 +1385,57 @@ issues, and list contents of libraries. calls a user defined function with specified arguments. It is equivalent to calling `function(args...)` directly except you can use it with function value, or name instead. This means you can pass functions to other user defined -functions as arguments and call them with `call` internally. And since function definitions return the defined +functions as arguments and call them with `call` internally. Since function definitions return the defined function, they can be defined in place as anonymous functions. +#### Passing function references to other modules of your application + +In case a function is defined by its name, Scarpet will attempt to resolve its definition in the given module and its imports, +meaning if the call is in a imported library, and not in the main module of your app, and that function is not visible from the +library perspective, but in the app, it won't be call-able. In case you pass a function name to a separate module in your app, +it should import back that method from the main module for visibility. + +Check an example of a problematic code of a library that expects a function value as a passed argument and how it is called in +the parent app: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme('foo' , 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` + +In this case `'foo'` will fail to dereference in `lib` as it is not visible by name. In tightly coupled modules, where `lib` is just +a component of your `app` you can use circular import to acknowledge the symbol from the other module (pretty much like +imports in Java classes), and that solves the issue but makes the library dependent on the main app: +``` +//lib.scl +import('app','foo'); +callme(fun, arg) -> call(fun, arg); +``` +You can circumvent that issue by explicitly dereferencing the local function where it is used as a lambda argument created +in the module in which the requested function is visible: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme(_(x) -> foo(x), 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` +Or by passing an explicit reference to the function, instead of calling it by name: +``` +//app.sc +import('lib', 'callme'); +global_foohandler = (foo(x) -> x*x); +test() -> callme(global_foohandler, 5); +``` + Little technical note: the use of `_` in expression passed to built in functions is much more efficient due to not creating new call stacks for each invoked function, but anonymous functions is the only mechanism available for programmers with their own lambda arguments @@ -1658,6 +1710,8 @@ map(range(10),_*_) => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] reduce(range(10),_a+_, 0) => 45 range(5,10) => [5, 6, 7, 8, 9] range(20, 10, -2) => [20, 18, 16, 14, 12] +range(-0.3, 0.3, 0.1) => [-0.3, -0.2, -0.1, 0, 0.1, 0.2] +range(0.3, -0.3, -0.1) => [0.3, 0.2, 0.1, -0, -0.1, -0.2] ## Map operations @@ -1705,7 +1759,9 @@ Here is the gist of the Minecraft related functions. Otherwise the CarpetScript ## 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 +folder, flat. In singleplayer, you can also save apps in `.minecraft/config/carpet/scripts` for them to be available in any world, +and here you can actually organize them in folders. +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). @@ -1731,15 +1787,14 @@ the app state and all players interactions run in the same context, sharing defi explicitly keyed with players, player names, uuids, etc. Even for `'player'` scoped apps, you can access specific player app with with commandblocks using `/execute as run script in run ...`. -To access global/server state for a player app, you need to disown the command from any player, +To access global/server state for a player app, which you shouldn't do, you need to disown the command from any player, so either use a command block, or any arbitrary entity: `/execute as @e[type=bat,limit=1] run script in globals` for instance, however -running anything in the global scope for a `'player'` scoped app is not recommended. -* `'stay_loaded'`: defaults to `false`. If true, and `/carpet scriptsAutoload` is turned on, the following apps will +running anything in the global scope for a `'player'` scoped app is not intended. +* `'stay_loaded'`: defaults to `true`. 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. + WARNING: all apps will run once at startup anyways, so be aware that their actions that are called +statically, will be performed once anyways. Only apps present in the world's `scripts` folder will be autoloaded. * `'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 @@ -1749,9 +1804,20 @@ conflicts and ambiguities between different paths of execution. While ambiguous 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. +* `'command_permission'` - indicates a custom permission to run the command. It can either be a number indicating +permission level (from 1 to 4) or a string value, one of: `'all'` (default), `'ops'` (default opped player with permission level of 2), +`'server'` - command accessible only through the server console and commandblocks, but not in chat, `'players'` - opposite +of the former, allowing only use in player chat. It can also be a function (lambda function or function value, not function name) +that takes 1 parameter, which represents the calling player, or `'null'` if the command represents a server call. +The function will prevent the command from running if it evaluates to `false`. +Please note, that Minecraft evaluates eligible commands for players when they join, or on reload/restart, so if you use a +predicate that is volatile and might change, the command might falsely do or do not indicate that it is available to the player, +however player will always be able to type it in and either succeed, or fail, based on their current permissions. +Custom permission applies to legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below. * `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well -as for the custom commands defined with `'commands'`, see below -* `'commands'` - defines custom commands for the app to be executed with `/` command, see below +as for the custom commands defined with `'commands'`, see below. +* `'commands'` - defines custom commands for the app to be executed with `/` command, see below. ## Custom app commands @@ -1904,6 +1970,20 @@ Here is a list of built-in types, with their return value formats, as well as a * `'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) + * `'blockpredicate`': returns a 4-tuple indicating conditions of a block to match: block name, block tag, + map of required state properties, and tag to match. Either block name or block tag are `null` but not both. + Property map is always specified, but its empty for no conditions, and matching nbt tag can be `null` indicating + no requirements. Technically the 'all-matching' predicate would be `[null, null, {}, null]`, but + block name or block tag is always specified. One can use the following routine to match a block agains this predicate: + ``` + block_to_match = block(x,y,z); + [block_name, block_tag, properties, nbt_tag] = block_predicate; + + (block_name == null || block_name == block_to_match) && + (block_tag == null || block_tags(block_to_match, block_tag)) && + all(properties, block_state(block_to_match, _) == properties:_) && + (!tag || tag_matches(block_data(block_to_match), tag)) + ``` * `'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. @@ -1992,18 +2072,19 @@ print(b); // 'stone', block was evaluated 'eagerly' but call to `block` All the functions below can be used with block value, queried with coord triple, or 3-long list. All `pos` in the functions referenced below refer to either method of passing block position. -### `set(pos, block, property?, value?, ..., block_data?)`, `set(pos, block, l(property?, value?, ...), block_data?)` +### `set(pos, block, property?, value?, ..., block_data?)`, `set(pos, block, [property?, value?, ...], block_data?)`, `set(pos, block, {property? -> value?, ...}, block_data?)` -First part of the `set` function is either a coord triple, list of three numbers, or other block with coordinates. -Second part, `block` is either block value as a result of `block()` function string value indicating the block name, -and optional `property - value` pairs for extra block properties. Optional `block_data` include the block data to +First argument for the `set` function is either a coord triple, list of three numbers, or a world localized block value. +Second argument, `block`, is either an existing block value, a result of `block()` function, or a string value indicating the block name +with optional state and block data. It is then followed by an optional +`property - value` pairs for extra block state (which can also be provided in a list or a map). Optional `block_data` include the block data to be set for the target block. If `block` is specified only by name, then if a destination block is the same the `set` operation is skipped, otherwise is executed, for other potential extra -properties. +properties that the original source block may have contained. -The returned value is either the block state that has been set, or `false` if block setting was skipped +The returned value is either the block state that has been set, or `false` if block setting was skipped, or failed
 set(0,5,0,'bedrock')  => bedrock
@@ -2019,11 +2100,15 @@ scan(0,100,0,20,20,20,set(_,block('glass')))
 b = block('glass'); scan(0,100,0,20,20,20,set(_,b))
     // yet another option, skips all parsing
 set(x,y,z,'iron_trapdoor')  // sets bottom iron trapdoor
-set(x,y,z,'iron_trapdoor[half=top]')  // Incorrect. sets bottom iron trapdoor - no parsing of properties
-set(x,y,z,'iron_trapdoor','half','top') // correct - top trapdoor
-set(x,y,z, block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing
+
+set(x,y,z,'iron_trapdoor[half=top]')  // sets the top trapdoor
+set(x,y,z,'iron_trapdoor','half','top') // also correct - top trapdoor
+set(x,y,z,'iron_trapdoor', ['half','top']) // same
+set(x,y,z,'iron_trapdoor', {'half' -> 'top'}) // same
+set(x,y,z, block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing of block state
+
 set(x,y,z,'hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') // extra block data
-set(x,y,z,'hopper', l('facing', 'north'), nbt('{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') ) // same
+set(x,y,z,'hopper', {'facing' -> 'north'}, nbt('{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') ) // same
 
### `without_updates(expr)` @@ -2168,6 +2253,16 @@ Causes a block to be harvested by a specified player entity. Honors player item tool if applicable. If the entity is not a valid player, no block gets destroyed. If a player is not allowed to break that block, a block doesn't get destroyed either. +### `weather()`,`weather(type)`,`weather(type, ticks)` + +If called with no args, returns `'clear'`, `'rain` or `'thunder'` based on the current weather. If thundering, will +always return `'thunder'`, if not will return `'rain'` or `'clear'` based on the current weather. + +With one arg, (either `'clear'`, `'rain` or `'thunder'`), returns the number of remaining ticks for that weather type. +NB: It can thunder without there being a thunderstorm, there has to be both rain and thunder to form a storm. + +With two args, sets the weather to `type` for `ticks` ticks. + ## Block and World querying ### `pos(block), pos(entity)` @@ -2738,14 +2833,16 @@ the future. These functions help scan larger areas of blocks without using generic loop functions, like nested `loop`. -### `scan(center, range, lower_range?, expr)` +### `scan(center, range, upper_range?, expr)` Evaluates expression over area of blocks defined by its center `center = (cx, cy, cz)`, expanded in all directions by `range = (dx, dy, dz)` blocks, or optionally in negative with `range` coords, and `upper_range` coords in -positive values. -`center` can be defined either as three coordinates, a list of three coords, or a block value. -`range` and `lower_range` can have the same representations, just if its a block, it computes the distance to the center +positive values, so you can use that if you know the lower coord, and dimension by calling `'scan(center, 0, 0, 0, w, h, d, ...)`. + +`center` can be defined either as three coordinates, a single tuple of three coords, or a block value. +`range` and `upper_range` can have the same representations, just if they are block values, it computes the distance to the center as range instead of taking the values as is. + `expr` receives `_x, _y, _z` as coords of current analyzed block and `_`, which represents the block itself. Returns number of successful evaluations of `expr` (with `true` boolean result) unless called in void context, @@ -2770,7 +2867,7 @@ Returns the list of 6 neighbouring blocks to the argument. Commonly used with ot for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block -### `rect(centre, range?, positive_range?)` +### `rect(centre, range?, upper_range?)` Returns an iterator, just like `range` function that iterates over a rectangular area of blocks. If only center point is specified, it iterates over 27 blocks. If `range` arguments are specified, expands selection by the respective @@ -2778,7 +2875,7 @@ number of blocks in each direction. If `positive_range` arguments are specified, it uses `range` for negative offset, and `positive_range` for positive. `centre` can be defined either as three coordinates, a list of three coords, or a block value. -`range` and `positive_range` can have the same representations, just if its a block, it computes the distance to the center +`range` and `positive_range` can have the same representations, just if they are block values, it computes the distance to the center as range instead of taking the values as is.` ### `diamond(centre_pos, radius?, height?)` @@ -2840,7 +2937,9 @@ to the `regular` group. * `monster`, `creature`, `ambient`, `water_creature`, `water_ambient`, `misc` - another categorization of living entities based on their spawn group. Negative descriptor resolves to all living types that don't belong to that category. -* Any of the following standard entity types (equivalent to selection from `/summon` vanilla command: +* All entity tags including those provided with datapacks. Built-in entity tags include: `skeletons`, `raiders`, +`beehive_inhabitors` (bee, duh), `arrows` and `impact_projectiles`. +* Any of the following standard entity types (equivalent to selection from `/summon` vanilla command: `area_effect_cloud`, `armor_stand`, `arrow`, `bat`, `bee`, `blaze`, `boat`, `cat`, `cave_spider`, `chest_minecart`, `chicken`, `cod`, `command_block_minecart`, `cow`, `creeper`, `dolphin`, `donkey`, `dragon_fireball`, `drowned`, `egg`, `elder_guardian`, `end_crystal`, `ender_dragon`, `ender_pearl`, `enderman`, `endermite`, `evoker`, @@ -2862,10 +2961,13 @@ belonging to that group. Returns entities of a specified type in an area centered on `center` and at most `distance` blocks away from -the center point. Uses the same `type` selectors as `entities_list`. +the center point/area. Uses the same `type` selectors as `entities_list`. `center` and `distance` can either be a triple of coordinates or three consecutive arguments for `entity_area`. `center` can -also be represented as a block, in this case the search box will be centered on the middle of the block. +also be represented as a block, in this case the search box will be centered on the middle of the block, or an entity - in this case +entire bounding box of the entity serves as a 'center' of search which is then expanded in all directions with the `'distance'` vector. + +In any case - returns all entities which bounding box collides with the bounding box defined by `'center'` and `'disteance'`. entity_area is simpler than `entity_selector` and runs about 20% faster, but is limited to predefined selectors and cuboid search area. @@ -2994,13 +3096,21 @@ List of entities riding the entity. Entity that `e` rides. -### `query(e, 'tags')` +### `query(e, 'scoreboard_tags')`, `query(e, 'tags')`(deprecated) -List of entity's tags. +List of entity's scoreboard tags. -### `query(e, 'has_tag',tag)` +### `query(e, 'has_scoreboard_tag',tag)`, `query(e, 'has_tag',tag)`(deprecated) -Boolean, true if the entity is marked with `tag`. +Boolean, true if the entity is marked with a `tag` scoreboad tag. + +### `query(e, 'entity_tags')` + +List of entity tags assigned to the type this entity represents. + +### `query(e, 'has_entity_tag', tag)` + +Returns `true` if the entity matches that entity tag, `false` if it doesn't, and `null` if the tag is not valid. ### `query(e, 'is_burning')` @@ -3154,6 +3264,11 @@ Player's ping in milliseconds, or `null` if its not a player. Player's permission level, or `null` if not applicable for this entity. +### `query(e, 'client_brand')` + +Returns recognized type of client of the client connected. Possible results include `'vanilla'`, or `'carpet '` where +version indicates the version of the connected carpet client. + ### `query(e, 'effect', name?)` Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short @@ -3176,6 +3291,18 @@ Number indicating remaining entity health, or `null` if not applicable. Retrieves player hunger related information. For non-players, returns `null`. +### `query(e, 'absorption')` + +Gets the absorption of the player (yellow hearts, e.g when having a golden apple.) + +### `query(e,'xp')` +### `query(e,'xp_level')` +### `query(e,'xp_progress')` +### `query(e,'score')` + +Numbers related to player's xp. `xp` is the overall xp player has, `xp_level` is the levels seen in the hotbar, +`xp_progress` is a float between 0 and 1 indicating the percentage of the xp bar filled, and `score` is the number displayed upon death + ### `query(e, 'air')` Number indicating remaining entity health, or `null` if not applicable. @@ -3486,6 +3613,20 @@ Will set entity on fire for `ticks` ticks. Set to 0 to extinguish. Modifies directly player raw hunger components. Has no effect on non-players +### `modify(e, 'absorption', value)` + +Sets the absorption value for the player. Each point is half a yellow heart. + +### `modify(e, 'add_xp', value)` +### `modify(e, 'xp_level', value)` +### `modify(e, 'xp_progress', value)` +### `modify(e, 'xp_score', value)` + +Manipulates player xp values - `'add_xp'` the method you probably want to use +to manipulate how much 'xp' an action should give. `'xp_score'` only affects the number you see when you die, and +`'xp_progress'` controls the xp progressbar above the hotbar, should take values from 0 to 1, but you can set it to any value, +maybe you will get a double, who knows. + ### `modify(e, 'air', ticks)` Modifies entity air @@ -3529,6 +3670,10 @@ Required arguments: `entity, amount, source, attacking_entity` It doesn't mean that all entity types will have a chance to execute a given event, but entities will not error when you attach an inapplicable event to it. +In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + ### `entity_load_handler(descriptor / descriptors, function)`, `entity_load_handler(descriptor / descriptors, call_name, ... args?)` Attaches a callback to when any entity matching the following type / types is loaded in the game, allowing to grab a handle @@ -3627,6 +3772,14 @@ an inventory, all API functions typically do nothing and return null. Most items returned are in the form of a triple of item name, count, and nbt or the extra data associated with an item. +### item_list(tag?) + +With no arguments, returns a list of all items in the game. With an item tag provided, list items matching the tag, or `null` if tag is not valid. + +### item_tags(item, tag?) + +Returns list of tags the item belongs to, or, if tag is provided, `true` if an item maches the tag, `false` if it doesn't and `null` if that's not a valid tag + ### `stack_limit(item)` Returns number indicating what is the stack limit for the item. Its typically 1 (non-stackable), 16 (like buckets), @@ -3800,10 +3953,8 @@ with `__on_` and has the required arguments, will be bound automatically of such function would result in unbinding the app from this event. In case of `player` scoped apps, -all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, cannot be handled -in `'player'` scoped app. - -`'global'` scoped apps will receive both global and player built-in events. +all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, that don't have a specific +player target will be executed multiple times, once for each player, and once in `'global'` scoped apps. Most built-in events strive to report right before they take an effect in the game. The purpose of that is that this give a choice for the programmer to handle them right away (as it happens, potentially affect the course of action by changing the @@ -3822,8 +3973,15 @@ if it accepts required number of parameters. ## Built-in global events -Handling global events is only allowed in apps with `'global'` scope. With the default scope (`'player'`) you -simply wouldn't know which player to hook up this event to. +Global events will be handled once per app that is with `'global'` scope. With `player` scoped apps, each player instance + is responsible independently from handling their events, so a global event may be executed multiple times for each player. + +### `__on_server_starts()` +Event triggers after world is loaded and after all startup apps have started. It won't be triggered with `/reload`. + +### `__on_server_shuts_down()` +Event triggers when the server started the shutdown process, before `__on_close()` is executed. Unlike `__on_close()`, it doesn't +trigger with `/reload`. ### `__on_tick()` Event triggers at the beginning of each tick, located in the overworld. You can use `in_dimension()` @@ -3900,7 +4058,12 @@ adjusted. ### `__on_player_interacts_with_entity(player, entity, hand)` Triggered when player right clicks (interacts) with an entity, even if the entity has no vanilla interaction with the player or the item they are holding. The event is invoked after receiving a packet from the client, before anything happens server side -with that interaction +with that interaction. + +### `__on_player_trades(player, entity, buy_left, buy_right, sell)` +Triggered when player trades with a merchant. The event is invoked after the server allow the trade, but before the inventory +changes and merchant updates its trade-uses counter. +The parameter `entity` can be `null` if the merchant is not an entity. ### `__on_player_collides_with_entity(player, entity)` Triggered every time a player - entity collisions are calculated, before effects of collisions are applied in the game. @@ -3996,8 +4159,11 @@ is handled before scoreboard values for these statistics are changed. App programmers can define and trigger their own custom events. Unlike built-in events, all custom events pass a single value as an argument, but this doesn't mean that they cannot pass a complex list, map, or nbt tag as a message. Each event signal is -either targetting all global context apps, if no target player has been identified, or only player scoped apps, if the target player -is specified. +either targetting all apps instances for all players, including global apps, if no target player has been identified, +or only player scoped apps, if the target player +is specified, running once for that player app. You cannot target global apps with player-targeted signals. Built-in events +do target global apps, since their first argument is clearly defined and passed. That may change in the future in case there is +a compelling argument to be able to target global apps with player scopes. Programmers can also handle built-in events the same way as custom events, as well as triggering built-in events, which I have have no idea why you would need that. The following snippets have the same effect: @@ -4021,11 +4187,11 @@ handle_event('player_breaks_block', null); And `signal_event` can be used as a trigger, called twice for player based built-in events ``` signal_event('player_breaks_block', player, player, block); // to target all player scoped apps -signal_event('player_breaks_block', null , player, block); // to target all global scoped apps +signal_event('player_breaks_block', null , player, block); // to target all global scoped apps and all player instances ``` -or (for global only events) +or (for global events) ``` -signal_event('tick') // trigger only global scoped apps with no extra arguments +signal_event('tick') // trigger all apps with a tick event ``` ### `handle_event(event, callback ...)` @@ -4039,6 +4205,8 @@ If extra arguments are provided, they will be appended to the argument list of t Returns `true` if subscription to the event was successful, or `false` if it failed (for instance wrong scope for built-in event, or incorect number of parameters for the event). +If a callback is specified as `null`, the given app (or player app instance )stops handling that event. +
 foo(a) -> print(a);
 handle_event('boohoo', 'foo');
@@ -4051,116 +4219,64 @@ handle_event('tick', _() -> foo('tick happened')); // built-in event
 handle_event('tick', null)  // nah, ima good, kthxbai
 
+In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + ### `signal_event(event, target_player?, ... args?)` -Fires a specific event. If the event does not exist (only `handle_event` creates missing mising new events), or provided argument list +Fires a specific event. If the event does not exist (only `handle_event` creates missing new events), or provided argument list was not matching the callee expected arguments, returns `null`, otherwise returns number of apps notified. If `target_player` is specified and not `null` triggers a player specific event, targetting only `player` scoped apps for that player. Apps with globals scope will not be notified even if they handle this event. -If the `target_player` is omitted or `null`, it will instead only target `global` scoped apps and skip `player` scoped. -Therefore each custom event trigger can trigger each app once, but calls to '`signal_event`' can be repeated to cover -all app scopes. Note that all built-in player events have a player as a first argument, so to trigger these events, you need to +If the `target_player` is omitted or `null`, it will target `global` scoped apps and all instances of `player` scoped apps. +Note that all built-in player events have a player as a first argument, so to trigger these events, you need to provide them twice - once to specify the target player scope and second - to provide as an argument to the handler function.
 signal_event('player_breaks_block', player, player, block); // to target all player scoped apps
-signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps
-signal_event('tick') // triggering tick event in all loaded apps
+signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps and all player instances
+signal_event('tick') // trigger all apps with a tick event
 
## Custom events example -The following example contains 4 apps, 2 of them defining a new event, `"player_farts"` triggered after 40 ticks of continuous -squatting. Note that this could call the recipient event handlers twice, since `signal_event` will be called twice, but -in this case the event is once called as a global event, and once as a player event, targeting each app once. Custom events is -a good idea to share common event detection code with one app, and using the outcome of these events in multiple places. -Obviously you would define each event handler once not twice in your app system. - -In this -case here we have two ways of detecting all players squatting for more than 40 ticks - one of those apps will produce cloud particles -in an apprpriate location, and the other will produce a sound when it happens, and they both will proceed to signal the same event, -thankfully for different scopes, otherwise other apps would trigger twice. +The following example shows how you can communicate between different instances of the same player scoped app. It important to note +that signals can trigger other apps as well, assuming the name of the event matches. In this case the request name is called +`tp_request` and is triggered with a command. + ``` -// detector1.sc -// detecting event in a tick loop -__config() -> {'scope' -> 'global'}; - -global_sneaks = {}; -__on_tick() -> for(player('all'), - if (_~'sneaking', - sneak_time = if (has(global_sneaks:_), tick_time()-global_sneaks:_ , global_sneaks:_=tick_time(); 0); - if (sneak_time > 20 && sneak_time < 40, - particle('cloud', pos(_) + [0, _~'height'/2, 0] - _~'look', 1, 0.1) - ); - if (sneak_time == 40, - signal_event('player_farts', _, _) // triggering all player scoped apps from a global app +// tpa.sc +global_requester = null; +__config() -> { + 'commands' -> { + '' -> _(to) -> signal_event('tp_request', to, player()), + 'accept' -> _() -> if(global_requester, + run('tp '+global_requester~'command_name'); + global_requester = null ) - , // else - delete(global_sneaks:_) - ) -) + }, + 'arguments' -> { + 'player' -> {'type' -> 'players', 'single' -> true} + } +}; +handle_event('tp_request', _(req) -> ( + global_requester = req; + print(player(), format( + 'w '+req+' requested to teleport to you. Click ', + 'yb here', '^yb here', '!/tpa accept', + 'w to accept it.' + )); +)); ``` -``` -// detector2.sc -// detecting events in a more appropriate way - player based and event based app -__on_player_starts_sneaking(p) -> -( - global_sneaking = tick_time(); - schedule(40, 'check', p); -); - -check(p) -> -if( global_sneaking && tick_time() - global_sneaking == 40, - sound('entity.shulker.shoot', pos(p), 1, 0.6, 'player'); - signal_event('player_farts', null, p) // trigerring all global apps from a player scoped app -); - -__on_player_stops_sneaking(p) -> (global_sneaking = false); -``` - -And then two other apps that meant to do something with this event. One mimics an explosion, and the other - grows crops around -the player. - -``` -client1.sc -__config() -> {'scope' -> 'global'}; - -on_fart(p, msg) -> -( - print(p+msg); - particle('explosion', pos(p)); - sound('entity.generic.explode', pos(p), 1, 0.5); -); - -handle_event('player_farts', 'on_fart', ' goes kaboom'); -``` - -``` -client2.sc -longsneak(p) -> -( - ppos = pos(p); - loop ( 500, - target = ppos+[rand(12),rand(8),rand(12)]-[rand(12),rand(8),rand(12)]; - if (material(target) == 'plant' && ticks_randomly(target), - particle('happy_villager', target, 2, 0.4) - ); - random_tick(target) - ) -); - -handle_event('player_farts', 'longsneak'); -``` - - ## `/script event` command -used to display current events and bounded functions. use `add_to` ro register new event, or `remove_from` to +used to display current events and bounded functions. use `add_to` to register a new event, or `remove_from` to unbind a specific function from an event. Function to be bounded to an event needs to have the same number of parameters as the action is attempting to bind to (see list above). All calls in modules loaded via `/script load` -that have functions listed above will be automatically bounded and unbounded when script is unloaded. +that handle specific built-in events will be automatically bounded, and unbounded when script is unloaded. # Scoreboard ### `scoreboard()`, `scoreboard(objective)`, `scoreboard(objective, key)`, `scoreboard(objective, key, value)` @@ -4267,6 +4383,65 @@ team_property('admin','seeFriendlyInvisibles',true) Make all players in 'a team_property('admin','deathMessageVisibility','hideForOtherTeams') Make all players in 'admin' see other admins even when invisible ``` +## `bossbar()`, `bossbar(id)`, `bossbar(id,property,value?)` + +Manage bossbars just like with the `/bossbar` command. + +Without any arguments, returns a list of all bossbars. + +When an id is specified, creates a bossbar with that `id` and returns the id of the created bossbar. +Bossbar ids need a namespace and a name. If no namespace is specified, it will automatically use `minecraft:`. +In that case you should keep track of the bossbar with the id that `bossbar(id)` returns, because a namespace may be added automatically. +If the id was invalid (for example by having more than one colon), returns `null`. +If the bossbar already exists, returns `false`. + +`bossbar('timer') => 'minecraft:timer'` (Adds the namespace `minecraft:` because none is specified) + +`bossbar('scarpet:test') => 'scarpet:test'` In this case there is already a namespace specified + +`bossbar('foo:bar:baz') => null` Invalid identifier + +`bossbar(id,property)` is used to query the `property` of a bossbar. + +`bossbar(id,property,value)` can modify the `property` of the bossbar to a specified `value`. + +Available properties are: + +* color: can be `'pink'`, `'blue'`, `'red'`, `'green'`, `'yellow'`, `'purple'` or `'white'` + +* style: can be `'progress'`, `'notched_6'`, `'notched_10'`, `'notched_12'` or `'notched_20'` + +* value: value of the bossbar progress + +* max: maximum value of the bossbar progress, by default 100 + +* name: Text to display above the bossbar, supports formatted text + +* visible: whether the bossbar is visible or not + +* players: List of players that can see the bossbar + +* add_player: add a player to the players that can see this bossbar, this can only be used for modifying (`value` must be present) + +* remove: remove this bossbar, no `value` required + +``` +bossbar('script:test','style','notched_12') +bossbar('script:test','value',74) +bossbar('script:test','name',format('rb Test')) -> Change text +bossbar('script:test','visible',false) -> removes visibility, but keeps players +bossbar('script:test','players',player('all')) -> Visible for all players +bossbar('script:test','players',player('Steve')) -> Visible for Steve only +bossbar('script:test','players',null) -> Invalid player, removing all players +bossbar('script:test','add_player',player('Alex')) -> Add Alex to the list of players that can see the bossbar +bossbar('script:test','remove') -> remove bossbar 'script:test' +for(bossbar(),bossbar(_,'remove')) -> remove all bossbars +``` + + + + + # Auxiliary aspects Collection of other methods that control smaller, yet still important aspects of the game @@ -4462,9 +4637,10 @@ Other value types will only be converted to tags (including NBT tags) if `force` extra treatment when loading them back from NBT, but using `force` true will always produce output / never produce an exception. -### `print(expr)` +### `print(expr)`, `print(player/player_list, expr)` Displays the result of the expression to the chat. Overrides default `scarpet` behaviour of sending everyting to stderr. +Can optionally define player or list of players to send the message to. ### `format(components, ...)`, `format(l(components, ...))` @@ -4544,13 +4720,15 @@ Available output types: ### `read_file(resource, type)` ### `delete_file(resource, type)` ### `write_file(resource, type, data, ...)` +### `list_files(resource, type)` With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its - content, or deletes the resource. + content, deletes the resource, or lists other files under this resource. Resource is identified by a path to the file. A path can contain letters, numbers, characters `-`, `+`, or `_`, and a folder separator: `'/'`. Any other characters are stripped -from the name. Empty descriptors are invalid. Do not add file extensions to the descriptor - extensions are inferred +from the name. Empty descriptors are invalid, except for `list_files` where it means the root folder. + Do not add file extensions to the descriptor - extensions are inferred based on the `type` of the file. Resources can be located in the app specific space, or a shared space for all the apps. Accessing of app-specific @@ -4565,33 +4743,26 @@ specific data directory is under `world/scripts/foo.data/...`, and shared data s The default no-name app, via `/script run` command can only save/load/read files from the shared space. -Functions return `null` if an error is encounter or no file is present (for read and delete operations). Returns `true` -for success writes and deletes, and requested data, based on the file type, for read operations. - -NBT files can be written once as they an store one tag at a time. Consecutive writes will overwrite previous data. - -Write operations to text files always result in appending to the existing file, so consecutive writes will increase -the size of the file and add data to it. Since files are closed after each write, sending multiple lines of data to -write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the -third argument. - * `write_file('temp', 'text', 'foo', 'bar', 'baz')` or - * write_file('temp', 'text', l('foo', 'bar', 'baz')) +Functions return `null` if an error is encounter or no file is present (for read, list and delete operations). Returns `true` +for success writes and deletes, and requested data, based on the file type, for read operations. It returns list of files +for folder listing. -To log a single line of string - -Supported values for resource `type` is: +Supported values for resource `type` are: * `nbt` - NBT tag * `text` - text resource with automatic newlines added * `raw` - text resource without implied newlines - * `shared_nbt`, `shared_text`, `shared_raw` - shared versions of the above + * `folder` - for `list_files` only - indicting folder listing instead of files + * `shared_nbt`, `shared_text`, `shared_raw`, `shared_folder` - shared versions of the above NBT files have extension `.nbt`, store one NBT tag, and return a NBT type value. Text files have `.txt` extension, stores multiple lines of text and returns lists of all lines from the file. With `write_file`, multiple lines can be sent to the file at once. The only difference between `raw` and `text` types are automatic newlines added after each -record to the file. +record to the file. Since files are closed after each write, sending multiple lines of data to +write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the +third argument.
-write_file('foo', 'shared_text, l('one', 'two'));
+write_file('foo', 'shared_text, ['one', 'two']);
 write_file('foo', 'shared_text', 'three\n', 'four\n');
 write_file('foo', 'shared_raw', 'five\n', 'six\n');
 
@@ -4718,6 +4889,9 @@ algorithm has taken in to account last time mobs spawned.
 Schedules a user defined function to run with a specified `delay` ticks of delay. Scheduled functions run at the end 
 of the tick, and they will run in order they were scheduled.
 
+In case you want to schedule a function that is not defined in your module, please read the tips on
+ "Passing function references to other modules of your application" section in the `call(...)` section.
+
 ### `statistic(player, category, entry)`
 
 Queries in-game statistics for certain values. Categories include:
@@ -4757,7 +4931,9 @@ Available options in the scarpet app space:
   * `world_path` - full path to the world saves folder
   * `world_folder` - name of the direct folder in the saves that holds world files
   * `world_carpet_rules` - returns all Carpet rules in a map form (`rule`->`value`). Note that the values are always returned as strings, so you can't do boolean comparisons directly. Includes rules from extensions with their namespace (`namespace:rule`->`value`). You can later listen to rule changes with the `on_carpet_rule_changes(rule, newValue)` event.
- 
+  * `world_gamerules` - returns all gamerules in a map form (`rule`->`value`). Like carpet rules, values are returned as strings, so you can use appropriate value conversions using `bool()` or `number()` to convert them to other values. Gamerules are read-only to discourage app programmers to mess up with the settings intentionally applied by server admins. Isn't that just super annoying when a datapack messes up with your gamerule settings? It is still possible to change them though using `run('gamerule ...`.
+
+
  Relevant gameplay related properties
   * `game_difficulty` - current difficulty of the game: `'peacefu'`, `'easy'`, `'normal'`, or `'hard'`
   * `game_hardcore` - boolean whether the game is in hardcore mode
@@ -4769,13 +4945,13 @@ Available options in the scarpet app space:
   * `game_version` - base version of the game
   
  Server related properties
-
  * `server_motd` - the motd of the server visible when joining
  * `server_ip` - IP adress of the game hosted
  * `server_whitelisted` - boolean indicating whether the access to the server is only for whitelisted players
  * `server_whitelist` - list of players allowed to log in
  * `server_banned_players` - list of banned player names
  * `server_banned_ips` - list of banned IP addresses
+ * `server_dev_environment` - boolean indicating whether this server is in a development environment.
  
  System related properties
  * `java_max_memory` - maximum allowed memory accessible by JVM
@@ -4785,7 +4961,11 @@ Available options in the scarpet app space:
  * `java_version` - version of Java
  * `java_bits` - number indicating how many bits the Java has, 32 or 64
  * `java_system_cpu_load` - current percentage of CPU used by the system
- * `java_process_cpu_load` - current percentage of CPU used by JVM# `/script run` command
+ * `java_process_cpu_load` - current percentage of CPU used by JVM
+ 
+ Scarpet related properties
+ * `scarpet_version` - returns the version of the carpet your scarpet comes with.
+# `/script run` command
 
 Primary way to input commands. The command executes in the context, position, and dimension of the executing player, 
 commandblock, etc... The command receives 4 variables, `x`, `y`, `z` and `p` indicating position and 
@@ -4795,13 +4975,15 @@ to simulate running commands in a different scope.
 
 # `/script load / unload  (global?)`, `/script in ` commands
 
-`load / unload` commands allow for very conventient way of writing your code, providing it to the game and 
-distribute with your worlds without the need of use of commandblocks. Just place your scarpet code in the 
-/scripts folder of your world files and make sure it ends with `.sc` extension. The good thing about editing that 
-code is that you can no only use normal editing without the need of marking of newlines, 
+`load / unload` commands allow for very convenient way of writing your code, providing it to the game and 
+distribute with your worlds without the need of use of commandblocks. Just place your Scarpet code in the 
+`/scripts` folder of your world files and make sure it ends with `.sc` extension. In singleplayer, you can 
+also save your scripts in `.minecraft/config/carpet/scripts` to make them available in any world.
+
+The good thing about editing that code is that you can not only use normal editing without the need of marking of newlines,  
 but you can also use comments in your code.
 
-a comment is anything that starts with a double slash, and continues to the end of the line:
+A comment is anything that starts with a double slash, and continues to the end of the line:
 
 
 foo = 1;
diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md
index e5e9209b..49a24255 100644
--- a/docs/scarpet/api/Auxiliary.md
+++ b/docs/scarpet/api/Auxiliary.md
@@ -193,9 +193,10 @@ Other value types will only be converted to tags (including NBT tags) if `force`
 extra treatment when loading them back from NBT, but using `force` true will always produce output / never 
 produce an exception.
 
-### `print(expr)`
+### `print(expr)`, `print(player/player_list, expr)`
 
 Displays the result of the expression to the chat. Overrides default `scarpet` behaviour of sending everyting to stderr.
+Can optionally define player or list of players to send the message to.
 
 ### `format(components, ...)`, `format(l(components, ...))`
 
@@ -275,13 +276,15 @@ Available output types:
 ### `read_file(resource, type)`
 ### `delete_file(resource, type)`
 ### `write_file(resource, type, data, ...)`
+### `list_files(resource, type)`
 
 With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its
- content, or deletes the resource.
+ content, deletes the resource, or lists other files under this resource.
 
 Resource is identified by a path to the file.  
 A path can contain letters, numbers, characters `-`, `+`, or `_`, and a folder separator: `'/'`. Any other characters are stripped
-from the name. Empty descriptors are invalid. Do not add file extensions to the descriptor - extensions are inferred
+from the name. Empty descriptors are invalid, except for `list_files` where it means the root folder.
+ Do not add file extensions to the descriptor - extensions are inferred
 based on the `type` of the file.
  
 Resources can be located in the app specific space, or a shared space for all the apps. Accessing of app-specific
@@ -296,33 +299,26 @@ specific data directory is under `world/scripts/foo.data/...`, and shared data s
 
 The default no-name app, via `/script run` command can only save/load/read files from the shared space.
 
-Functions return `null` if an error is encounter or no file is present (for read and delete operations). Returns `true`
-for success writes and deletes, and requested data, based on the file type, for read operations.
-
-NBT files can be written once as they an store one tag at a time. Consecutive writes will overwrite previous data.
-
-Write operations to text files always result in appending to the existing file, so consecutive writes will increase
-the size of the file and add data to it. Since files are closed after each write, sending multiple lines of data to
-write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the
-third argument.
- * `write_file('temp', 'text', 'foo', 'bar', 'baz')` or
- * write_file('temp', 'text', l('foo', 'bar', 'baz'))
+Functions return `null` if an error is encounter or no file is present (for read, list and delete operations). Returns `true`
+for success writes and deletes, and requested data, based on the file type, for read operations. It returns list of files 
+for folder listing.
  
-To log a single line of string
- 
-Supported values for resource `type` is:
+Supported values for resource `type` are:
  * `nbt` - NBT tag
  * `text` - text resource with automatic newlines added
  * `raw` - text resource without implied newlines
- * `shared_nbt`, `shared_text`, `shared_raw` - shared versions of the above
+ * `folder` - for `list_files` only - indicting folder listing instead of files
+ * `shared_nbt`, `shared_text`, `shared_raw`, `shared_folder` - shared versions of the above
  
 NBT files have extension `.nbt`, store one NBT tag, and return a NBT type value. Text files have `.txt` extension, 
 stores multiple lines of text and returns lists of all lines from the file. With `write_file`, multiple lines can be
 sent to the file at once. The only difference between `raw` and `text` types are automatic newlines added after each
-record to the file.
+record to the file. Since files are closed after each write, sending multiple lines of data to
+write is beneficial for writing speed. To send multiple packs of data, either provide them flat or as a list in the
+third argument.
 
 
-write_file('foo', 'shared_text, l('one', 'two'));
+write_file('foo', 'shared_text, ['one', 'two']);
 write_file('foo', 'shared_text', 'three\n', 'four\n');
 write_file('foo', 'shared_raw', 'five\n', 'six\n');
 
@@ -449,6 +445,9 @@ algorithm has taken in to account last time mobs spawned.
 Schedules a user defined function to run with a specified `delay` ticks of delay. Scheduled functions run at the end 
 of the tick, and they will run in order they were scheduled.
 
+In case you want to schedule a function that is not defined in your module, please read the tips on
+ "Passing function references to other modules of your application" section in the `call(...)` section.
+
 ### `statistic(player, category, entry)`
 
 Queries in-game statistics for certain values. Categories include:
@@ -488,7 +487,9 @@ Available options in the scarpet app space:
   * `world_path` - full path to the world saves folder
   * `world_folder` - name of the direct folder in the saves that holds world files
   * `world_carpet_rules` - returns all Carpet rules in a map form (`rule`->`value`). Note that the values are always returned as strings, so you can't do boolean comparisons directly. Includes rules from extensions with their namespace (`namespace:rule`->`value`). You can later listen to rule changes with the `on_carpet_rule_changes(rule, newValue)` event.
- 
+  * `world_gamerules` - returns all gamerules in a map form (`rule`->`value`). Like carpet rules, values are returned as strings, so you can use appropriate value conversions using `bool()` or `number()` to convert them to other values. Gamerules are read-only to discourage app programmers to mess up with the settings intentionally applied by server admins. Isn't that just super annoying when a datapack messes up with your gamerule settings? It is still possible to change them though using `run('gamerule ...`.
+
+
  Relevant gameplay related properties
   * `game_difficulty` - current difficulty of the game: `'peacefu'`, `'easy'`, `'normal'`, or `'hard'`
   * `game_hardcore` - boolean whether the game is in hardcore mode
@@ -500,13 +501,13 @@ Available options in the scarpet app space:
   * `game_version` - base version of the game
   
  Server related properties
-
  * `server_motd` - the motd of the server visible when joining
  * `server_ip` - IP adress of the game hosted
  * `server_whitelisted` - boolean indicating whether the access to the server is only for whitelisted players
  * `server_whitelist` - list of players allowed to log in
  * `server_banned_players` - list of banned player names
  * `server_banned_ips` - list of banned IP addresses
+ * `server_dev_environment` - boolean indicating whether this server is in a development environment.
  
  System related properties
  * `java_max_memory` - maximum allowed memory accessible by JVM
@@ -516,4 +517,7 @@ Available options in the scarpet app space:
  * `java_version` - version of Java
  * `java_bits` - number indicating how many bits the Java has, 32 or 64
  * `java_system_cpu_load` - current percentage of CPU used by the system
- * `java_process_cpu_load` - current percentage of CPU used by JVM
\ No newline at end of file
+ * `java_process_cpu_load` - current percentage of CPU used by JVM
+ 
+ Scarpet related properties
+ * `scarpet_version` - returns the version of the carpet your scarpet comes with.
diff --git a/docs/scarpet/api/BlockIterations.md b/docs/scarpet/api/BlockIterations.md
index 29d39cf3..1a5e6091 100644
--- a/docs/scarpet/api/BlockIterations.md
+++ b/docs/scarpet/api/BlockIterations.md
@@ -2,14 +2,16 @@
 
 These functions help scan larger areas of blocks without using generic loop functions, like nested `loop`.
 
-### `scan(center, range, lower_range?, expr)`
+### `scan(center, range, upper_range?, expr)`
 
 Evaluates expression over area of blocks defined by its center `center = (cx, cy, cz)`, expanded in all directions 
 by `range = (dx, dy, dz)` blocks, or optionally in negative with `range` coords, and `upper_range` coords in 
-positive values.
-`center` can be defined either as three coordinates, a list of three coords, or a block value.
-`range` and `lower_range` can have the same representations, just if its a block, it computes the distance to the center
+positive values, so you can use that if you know the lower coord, and dimension by calling `'scan(center, 0, 0, 0, w, h, d, ...)`.
+
+`center` can be defined either as three coordinates, a single tuple of three coords, or a block value.
+`range` and `upper_range` can have the same representations, just if they are block values, it computes the distance to the center
 as range instead of taking the values as is.
+
 `expr` receives `_x, _y, _z` as coords of current analyzed block and `_`, which represents the block itself.
 
 Returns number of successful evaluations of `expr` (with `true` boolean result) unless called in void context, 
@@ -34,7 +36,7 @@ Returns the list of 6 neighbouring blocks to the argument. Commonly used with ot
 for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block
 
-### `rect(centre, range?, positive_range?)` +### `rect(centre, range?, upper_range?)` Returns an iterator, just like `range` function that iterates over a rectangular area of blocks. If only center point is specified, it iterates over 27 blocks. If `range` arguments are specified, expands selection by the respective @@ -42,7 +44,7 @@ number of blocks in each direction. If `positive_range` arguments are specified, it uses `range` for negative offset, and `positive_range` for positive. `centre` can be defined either as three coordinates, a list of three coords, or a block value. -`range` and `positive_range` can have the same representations, just if its a block, it computes the distance to the center +`range` and `positive_range` can have the same representations, just if they are block values, it computes the distance to the center as range instead of taking the values as is.` ### `diamond(centre_pos, radius?, height?)` diff --git a/docs/scarpet/api/BlocksAndWorldAccess.md b/docs/scarpet/api/BlocksAndWorldAccess.md index 8f295ee0..a7419d0f 100644 --- a/docs/scarpet/api/BlocksAndWorldAccess.md +++ b/docs/scarpet/api/BlocksAndWorldAccess.md @@ -36,18 +36,19 @@ print(b); // 'stone', block was evaluated 'eagerly' but call to `block` All the functions below can be used with block value, queried with coord triple, or 3-long list. All `pos` in the functions referenced below refer to either method of passing block position. -### `set(pos, block, property?, value?, ..., block_data?)`, `set(pos, block, l(property?, value?, ...), block_data?)` +### `set(pos, block, property?, value?, ..., block_data?)`, `set(pos, block, [property?, value?, ...], block_data?)`, `set(pos, block, {property? -> value?, ...}, block_data?)` -First part of the `set` function is either a coord triple, list of three numbers, or other block with coordinates. -Second part, `block` is either block value as a result of `block()` function string value indicating the block name, -and optional `property - value` pairs for extra block properties. Optional `block_data` include the block data to +First argument for the `set` function is either a coord triple, list of three numbers, or a world localized block value. +Second argument, `block`, is either an existing block value, a result of `block()` function, or a string value indicating the block name +with optional state and block data. It is then followed by an optional +`property - value` pairs for extra block state (which can also be provided in a list or a map). Optional `block_data` include the block data to be set for the target block. If `block` is specified only by name, then if a destination block is the same the `set` operation is skipped, otherwise is executed, for other potential extra -properties. +properties that the original source block may have contained. -The returned value is either the block state that has been set, or `false` if block setting was skipped +The returned value is either the block state that has been set, or `false` if block setting was skipped, or failed
 set(0,5,0,'bedrock')  => bedrock
@@ -63,11 +64,15 @@ scan(0,100,0,20,20,20,set(_,block('glass')))
 b = block('glass'); scan(0,100,0,20,20,20,set(_,b))
     // yet another option, skips all parsing
 set(x,y,z,'iron_trapdoor')  // sets bottom iron trapdoor
-set(x,y,z,'iron_trapdoor[half=top]')  // Incorrect. sets bottom iron trapdoor - no parsing of properties
-set(x,y,z,'iron_trapdoor','half','top') // correct - top trapdoor
-set(x,y,z, block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing
+
+set(x,y,z,'iron_trapdoor[half=top]')  // sets the top trapdoor
+set(x,y,z,'iron_trapdoor','half','top') // also correct - top trapdoor
+set(x,y,z,'iron_trapdoor', ['half','top']) // same
+set(x,y,z,'iron_trapdoor', {'half' -> 'top'}) // same
+set(x,y,z, block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing of block state
+
 set(x,y,z,'hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') // extra block data
-set(x,y,z,'hopper', l('facing', 'north'), nbt('{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') ) // same
+set(x,y,z,'hopper', {'facing' -> 'north'}, nbt('{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') ) // same
 
### `without_updates(expr)` @@ -212,6 +217,16 @@ Causes a block to be harvested by a specified player entity. Honors player item tool if applicable. If the entity is not a valid player, no block gets destroyed. If a player is not allowed to break that block, a block doesn't get destroyed either. +### `weather()`,`weather(type)`,`weather(type, ticks)` + +If called with no args, returns `'clear'`, `'rain` or `'thunder'` based on the current weather. If thundering, will +always return `'thunder'`, if not will return `'rain'` or `'clear'` based on the current weather. + +With one arg, (either `'clear'`, `'rain` or `'thunder'`), returns the number of remaining ticks for that weather type. +NB: It can thunder without there being a thunderstorm, there has to be both rain and thunder to form a storm. + +With two args, sets the weather to `type` for `ticks` ticks. + ## Block and World querying ### `pos(block), pos(entity)` diff --git a/docs/scarpet/api/Entities.md b/docs/scarpet/api/Entities.md index a60d426c..0ebc7a59 100644 --- a/docs/scarpet/api/Entities.md +++ b/docs/scarpet/api/Entities.md @@ -52,7 +52,9 @@ to the `regular` group. * `monster`, `creature`, `ambient`, `water_creature`, `water_ambient`, `misc` - another categorization of living entities based on their spawn group. Negative descriptor resolves to all living types that don't belong to that category. -* Any of the following standard entity types (equivalent to selection from `/summon` vanilla command: +* All entity tags including those provided with datapacks. Built-in entity tags include: `skeletons`, `raiders`, +`beehive_inhabitors` (bee, duh), `arrows` and `impact_projectiles`. +* Any of the following standard entity types (equivalent to selection from `/summon` vanilla command: `area_effect_cloud`, `armor_stand`, `arrow`, `bat`, `bee`, `blaze`, `boat`, `cat`, `cave_spider`, `chest_minecart`, `chicken`, `cod`, `command_block_minecart`, `cow`, `creeper`, `dolphin`, `donkey`, `dragon_fireball`, `drowned`, `egg`, `elder_guardian`, `end_crystal`, `ender_dragon`, `ender_pearl`, `enderman`, `endermite`, `evoker`, @@ -74,10 +76,13 @@ belonging to that group. Returns entities of a specified type in an area centered on `center` and at most `distance` blocks away from -the center point. Uses the same `type` selectors as `entities_list`. +the center point/area. Uses the same `type` selectors as `entities_list`. `center` and `distance` can either be a triple of coordinates or three consecutive arguments for `entity_area`. `center` can -also be represented as a block, in this case the search box will be centered on the middle of the block. +also be represented as a block, in this case the search box will be centered on the middle of the block, or an entity - in this case +entire bounding box of the entity serves as a 'center' of search which is then expanded in all directions with the `'distance'` vector. + +In any case - returns all entities which bounding box collides with the bounding box defined by `'center'` and `'disteance'`. entity_area is simpler than `entity_selector` and runs about 20% faster, but is limited to predefined selectors and cuboid search area. @@ -206,13 +211,21 @@ List of entities riding the entity. Entity that `e` rides. -### `query(e, 'tags')` +### `query(e, 'scoreboard_tags')`, `query(e, 'tags')`(deprecated) -List of entity's tags. +List of entity's scoreboard tags. -### `query(e, 'has_tag',tag)` +### `query(e, 'has_scoreboard_tag',tag)`, `query(e, 'has_tag',tag)`(deprecated) -Boolean, true if the entity is marked with `tag`. +Boolean, true if the entity is marked with a `tag` scoreboad tag. + +### `query(e, 'entity_tags')` + +List of entity tags assigned to the type this entity represents. + +### `query(e, 'has_entity_tag', tag)` + +Returns `true` if the entity matches that entity tag, `false` if it doesn't, and `null` if the tag is not valid. ### `query(e, 'is_burning')` @@ -366,6 +379,11 @@ Player's ping in milliseconds, or `null` if its not a player. Player's permission level, or `null` if not applicable for this entity. +### `query(e, 'client_brand')` + +Returns recognized type of client of the client connected. Possible results include `'vanilla'`, or `'carpet '` where +version indicates the version of the connected carpet client. + ### `query(e, 'effect', name?)` Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short @@ -388,6 +406,18 @@ Number indicating remaining entity health, or `null` if not applicable. Retrieves player hunger related information. For non-players, returns `null`. +### `query(e, 'absorption')` + +Gets the absorption of the player (yellow hearts, e.g when having a golden apple.) + +### `query(e,'xp')` +### `query(e,'xp_level')` +### `query(e,'xp_progress')` +### `query(e,'score')` + +Numbers related to player's xp. `xp` is the overall xp player has, `xp_level` is the levels seen in the hotbar, +`xp_progress` is a float between 0 and 1 indicating the percentage of the xp bar filled, and `score` is the number displayed upon death + ### `query(e, 'air')` Number indicating remaining entity health, or `null` if not applicable. @@ -698,6 +728,20 @@ Will set entity on fire for `ticks` ticks. Set to 0 to extinguish. Modifies directly player raw hunger components. Has no effect on non-players +### `modify(e, 'absorption', value)` + +Sets the absorption value for the player. Each point is half a yellow heart. + +### `modify(e, 'add_xp', value)` +### `modify(e, 'xp_level', value)` +### `modify(e, 'xp_progress', value)` +### `modify(e, 'xp_score', value)` + +Manipulates player xp values - `'add_xp'` the method you probably want to use +to manipulate how much 'xp' an action should give. `'xp_score'` only affects the number you see when you die, and +`'xp_progress'` controls the xp progressbar above the hotbar, should take values from 0 to 1, but you can set it to any value, +maybe you will get a double, who knows. + ### `modify(e, 'air', ticks)` Modifies entity air @@ -741,6 +785,10 @@ Required arguments: `entity, amount, source, attacking_entity` It doesn't mean that all entity types will have a chance to execute a given event, but entities will not error when you attach an inapplicable event to it. +In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + ### `entity_load_handler(descriptor / descriptors, function)`, `entity_load_handler(descriptor / descriptors, call_name, ... args?)` Attaches a callback to when any entity matching the following type / types is loaded in the game, allowing to grab a handle diff --git a/docs/scarpet/api/Events.md b/docs/scarpet/api/Events.md index 2f343835..8ff9a8b7 100644 --- a/docs/scarpet/api/Events.md +++ b/docs/scarpet/api/Events.md @@ -9,10 +9,8 @@ with `__on_` and has the required arguments, will be bound automatically of such function would result in unbinding the app from this event. In case of `player` scoped apps, -all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, cannot be handled -in `'player'` scoped app. - -`'global'` scoped apps will receive both global and player built-in events. +all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, that don't have a specific +player target will be executed multiple times, once for each player, and once in `'global'` scoped apps. Most built-in events strive to report right before they take an effect in the game. The purpose of that is that this give a choice for the programmer to handle them right away (as it happens, potentially affect the course of action by changing the @@ -31,8 +29,15 @@ if it accepts required number of parameters. ## Built-in global events -Handling global events is only allowed in apps with `'global'` scope. With the default scope (`'player'`) you -simply wouldn't know which player to hook up this event to. +Global events will be handled once per app that is with `'global'` scope. With `player` scoped apps, each player instance + is responsible independently from handling their events, so a global event may be executed multiple times for each player. + +### `__on_server_starts()` +Event triggers after world is loaded and after all startup apps have started. It won't be triggered with `/reload`. + +### `__on_server_shuts_down()` +Event triggers when the server started the shutdown process, before `__on_close()` is executed. Unlike `__on_close()`, it doesn't +trigger with `/reload`. ### `__on_tick()` Event triggers at the beginning of each tick, located in the overworld. You can use `in_dimension()` @@ -109,7 +114,12 @@ adjusted. ### `__on_player_interacts_with_entity(player, entity, hand)` Triggered when player right clicks (interacts) with an entity, even if the entity has no vanilla interaction with the player or the item they are holding. The event is invoked after receiving a packet from the client, before anything happens server side -with that interaction +with that interaction. + +### `__on_player_trades(player, entity, buy_left, buy_right, sell)` +Triggered when player trades with a merchant. The event is invoked after the server allow the trade, but before the inventory +changes and merchant updates its trade-uses counter. +The parameter `entity` can be `null` if the merchant is not an entity. ### `__on_player_collides_with_entity(player, entity)` Triggered every time a player - entity collisions are calculated, before effects of collisions are applied in the game. @@ -205,8 +215,11 @@ is handled before scoreboard values for these statistics are changed. App programmers can define and trigger their own custom events. Unlike built-in events, all custom events pass a single value as an argument, but this doesn't mean that they cannot pass a complex list, map, or nbt tag as a message. Each event signal is -either targetting all global context apps, if no target player has been identified, or only player scoped apps, if the target player -is specified. +either targetting all apps instances for all players, including global apps, if no target player has been identified, +or only player scoped apps, if the target player +is specified, running once for that player app. You cannot target global apps with player-targeted signals. Built-in events +do target global apps, since their first argument is clearly defined and passed. That may change in the future in case there is +a compelling argument to be able to target global apps with player scopes. Programmers can also handle built-in events the same way as custom events, as well as triggering built-in events, which I have have no idea why you would need that. The following snippets have the same effect: @@ -230,11 +243,11 @@ handle_event('player_breaks_block', null); And `signal_event` can be used as a trigger, called twice for player based built-in events ``` signal_event('player_breaks_block', player, player, block); // to target all player scoped apps -signal_event('player_breaks_block', null , player, block); // to target all global scoped apps +signal_event('player_breaks_block', null , player, block); // to target all global scoped apps and all player instances ``` -or (for global only events) +or (for global events) ``` -signal_event('tick') // trigger only global scoped apps with no extra arguments +signal_event('tick') // trigger all apps with a tick event ``` ### `handle_event(event, callback ...)` @@ -248,6 +261,8 @@ If extra arguments are provided, they will be appended to the argument list of t Returns `true` if subscription to the event was successful, or `false` if it failed (for instance wrong scope for built-in event, or incorect number of parameters for the event). +If a callback is specified as `null`, the given app (or player app instance )stops handling that event. +
 foo(a) -> print(a);
 handle_event('boohoo', 'foo');
@@ -260,113 +275,61 @@ handle_event('tick', _() -> foo('tick happened')); // built-in event
 handle_event('tick', null)  // nah, ima good, kthxbai
 
+In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + ### `signal_event(event, target_player?, ... args?)` -Fires a specific event. If the event does not exist (only `handle_event` creates missing mising new events), or provided argument list +Fires a specific event. If the event does not exist (only `handle_event` creates missing new events), or provided argument list was not matching the callee expected arguments, returns `null`, otherwise returns number of apps notified. If `target_player` is specified and not `null` triggers a player specific event, targetting only `player` scoped apps for that player. Apps with globals scope will not be notified even if they handle this event. -If the `target_player` is omitted or `null`, it will instead only target `global` scoped apps and skip `player` scoped. -Therefore each custom event trigger can trigger each app once, but calls to '`signal_event`' can be repeated to cover -all app scopes. Note that all built-in player events have a player as a first argument, so to trigger these events, you need to +If the `target_player` is omitted or `null`, it will target `global` scoped apps and all instances of `player` scoped apps. +Note that all built-in player events have a player as a first argument, so to trigger these events, you need to provide them twice - once to specify the target player scope and second - to provide as an argument to the handler function.
 signal_event('player_breaks_block', player, player, block); // to target all player scoped apps
-signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps
-signal_event('tick') // triggering tick event in all loaded apps
+signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps and all player instances
+signal_event('tick') // trigger all apps with a tick event
 
## Custom events example -The following example contains 4 apps, 2 of them defining a new event, `"player_farts"` triggered after 40 ticks of continuous -squatting. Note that this could call the recipient event handlers twice, since `signal_event` will be called twice, but -in this case the event is once called as a global event, and once as a player event, targeting each app once. Custom events is -a good idea to share common event detection code with one app, and using the outcome of these events in multiple places. -Obviously you would define each event handler once not twice in your app system. - -In this -case here we have two ways of detecting all players squatting for more than 40 ticks - one of those apps will produce cloud particles -in an apprpriate location, and the other will produce a sound when it happens, and they both will proceed to signal the same event, -thankfully for different scopes, otherwise other apps would trigger twice. +The following example shows how you can communicate between different instances of the same player scoped app. It important to note +that signals can trigger other apps as well, assuming the name of the event matches. In this case the request name is called +`tp_request` and is triggered with a command. + ``` -// detector1.sc -// detecting event in a tick loop -__config() -> {'scope' -> 'global'}; - -global_sneaks = {}; -__on_tick() -> for(player('all'), - if (_~'sneaking', - sneak_time = if (has(global_sneaks:_), tick_time()-global_sneaks:_ , global_sneaks:_=tick_time(); 0); - if (sneak_time > 20 && sneak_time < 40, - particle('cloud', pos(_) + [0, _~'height'/2, 0] - _~'look', 1, 0.1) - ); - if (sneak_time == 40, - signal_event('player_farts', _, _) // triggering all player scoped apps from a global app +// tpa.sc +global_requester = null; +__config() -> { + 'commands' -> { + '' -> _(to) -> signal_event('tp_request', to, player()), + 'accept' -> _() -> if(global_requester, + run('tp '+global_requester~'command_name'); + global_requester = null ) - , // else - delete(global_sneaks:_) - ) -) + }, + 'arguments' -> { + 'player' -> {'type' -> 'players', 'single' -> true} + } +}; +handle_event('tp_request', _(req) -> ( + global_requester = req; + print(player(), format( + 'w '+req+' requested to teleport to you. Click ', + 'yb here', '^yb here', '!/tpa accept', + 'w to accept it.' + )); +)); ``` -``` -// detector2.sc -// detecting events in a more appropriate way - player based and event based app -__on_player_starts_sneaking(p) -> -( - global_sneaking = tick_time(); - schedule(40, 'check', p); -); - -check(p) -> -if( global_sneaking && tick_time() - global_sneaking == 40, - sound('entity.shulker.shoot', pos(p), 1, 0.6, 'player'); - signal_event('player_farts', null, p) // trigerring all global apps from a player scoped app -); - -__on_player_stops_sneaking(p) -> (global_sneaking = false); -``` - -And then two other apps that meant to do something with this event. One mimics an explosion, and the other - grows crops around -the player. - -``` -client1.sc -__config() -> {'scope' -> 'global'}; - -on_fart(p, msg) -> -( - print(p+msg); - particle('explosion', pos(p)); - sound('entity.generic.explode', pos(p), 1, 0.5); -); - -handle_event('player_farts', 'on_fart', ' goes kaboom'); -``` - -``` -client2.sc -longsneak(p) -> -( - ppos = pos(p); - loop ( 500, - target = ppos+[rand(12),rand(8),rand(12)]-[rand(12),rand(8),rand(12)]; - if (material(target) == 'plant' && ticks_randomly(target), - particle('happy_villager', target, 2, 0.4) - ); - random_tick(target) - ) -); - -handle_event('player_farts', 'longsneak'); -``` - - ## `/script event` command -used to display current events and bounded functions. use `add_to` ro register new event, or `remove_from` to +used to display current events and bounded functions. use `add_to` to register a new event, or `remove_from` to unbind a specific function from an event. Function to be bounded to an event needs to have the same number of parameters as the action is attempting to bind to (see list above). All calls in modules loaded via `/script load` -that have functions listed above will be automatically bounded and unbounded when script is unloaded. +that handle specific built-in events will be automatically bounded, and unbounded when script is unloaded. diff --git a/docs/scarpet/api/Inventories.md b/docs/scarpet/api/Inventories.md index 1ecc692b..ebe2ed7b 100644 --- a/docs/scarpet/api/Inventories.md +++ b/docs/scarpet/api/Inventories.md @@ -28,6 +28,14 @@ an inventory, all API functions typically do nothing and return null. Most items returned are in the form of a triple of item name, count, and nbt or the extra data associated with an item. +### item_list(tag?) + +With no arguments, returns a list of all items in the game. With an item tag provided, list items matching the tag, or `null` if tag is not valid. + +### item_tags(item, tag?) + +Returns list of tags the item belongs to, or, if tag is provided, `true` if an item maches the tag, `false` if it doesn't and `null` if that's not a valid tag + ### `stack_limit(item)` Returns number indicating what is the stack limit for the item. Its typically 1 (non-stackable), 16 (like buckets), diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md index 9e9fc5bd..7ec68245 100644 --- a/docs/scarpet/api/Overview.md +++ b/docs/scarpet/api/Overview.md @@ -6,7 +6,9 @@ Here is the gist of the Minecraft related functions. Otherwise the CarpetScript ## 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 +folder, flat. In singleplayer, you can also save apps in `.minecraft/config/carpet/scripts` for them to be available in any world, +and here you can actually organize them in folders. +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). @@ -32,15 +34,14 @@ the app state and all players interactions run in the same context, sharing defi explicitly keyed with players, player names, uuids, etc. Even for `'player'` scoped apps, you can access specific player app with with commandblocks using `/execute as run script in run ...`. -To access global/server state for a player app, you need to disown the command from any player, +To access global/server state for a player app, which you shouldn't do, you need to disown the command from any player, so either use a command block, or any arbitrary entity: `/execute as @e[type=bat,limit=1] run script in globals` for instance, however -running anything in the global scope for a `'player'` scoped app is not recommended. -* `'stay_loaded'`: defaults to `false`. If true, and `/carpet scriptsAutoload` is turned on, the following apps will +running anything in the global scope for a `'player'` scoped app is not intended. +* `'stay_loaded'`: defaults to `true`. 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. + WARNING: all apps will run once at startup anyways, so be aware that their actions that are called +statically, will be performed once anyways. Only apps present in the world's `scripts` folder will be autoloaded. * `'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 @@ -50,9 +51,20 @@ conflicts and ambiguities between different paths of execution. While ambiguous 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. +* `'command_permission'` - indicates a custom permission to run the command. It can either be a number indicating +permission level (from 1 to 4) or a string value, one of: `'all'` (default), `'ops'` (default opped player with permission level of 2), +`'server'` - command accessible only through the server console and commandblocks, but not in chat, `'players'` - opposite +of the former, allowing only use in player chat. It can also be a function (lambda function or function value, not function name) +that takes 1 parameter, which represents the calling player, or `'null'` if the command represents a server call. +The function will prevent the command from running if it evaluates to `false`. +Please note, that Minecraft evaluates eligible commands for players when they join, or on reload/restart, so if you use a +predicate that is volatile and might change, the command might falsely do or do not indicate that it is available to the player, +however player will always be able to type it in and either succeed, or fail, based on their current permissions. +Custom permission applies to legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below. * `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well -as for the custom commands defined with `'commands'`, see below -* `'commands'` - defines custom commands for the app to be executed with `/` command, see below +as for the custom commands defined with `'commands'`, see below. +* `'commands'` - defines custom commands for the app to be executed with `/` command, see below. ## Custom app commands @@ -205,6 +217,20 @@ Here is a list of built-in types, with their return value formats, as well as a * `'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) + * `'blockpredicate`': returns a 4-tuple indicating conditions of a block to match: block name, block tag, + map of required state properties, and tag to match. Either block name or block tag are `null` but not both. + Property map is always specified, but its empty for no conditions, and matching nbt tag can be `null` indicating + no requirements. Technically the 'all-matching' predicate would be `[null, null, {}, null]`, but + block name or block tag is always specified. One can use the following routine to match a block agains this predicate: + ``` + block_to_match = block(x,y,z); + [block_name, block_tag, properties, nbt_tag] = block_predicate; + + (block_name == null || block_name == block_to_match) && + (block_tag == null || block_tags(block_to_match, block_tag)) && + all(properties, block_state(block_to_match, _) == properties:_) && + (!tag || tag_matches(block_data(block_to_match), tag)) + ``` * `'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. diff --git a/docs/scarpet/api/Scoreboard.md b/docs/scarpet/api/Scoreboard.md index 51a3fd2b..b1b2b2d5 100644 --- a/docs/scarpet/api/Scoreboard.md +++ b/docs/scarpet/api/Scoreboard.md @@ -105,3 +105,62 @@ team_property('admin','seeFriendlyInvisibles',true) Make all players in 'a team_property('admin','deathMessageVisibility','hideForOtherTeams') Make all players in 'admin' see other admins even when invisible ``` +## `bossbar()`, `bossbar(id)`, `bossbar(id,property,value?)` + +Manage bossbars just like with the `/bossbar` command. + +Without any arguments, returns a list of all bossbars. + +When an id is specified, creates a bossbar with that `id` and returns the id of the created bossbar. +Bossbar ids need a namespace and a name. If no namespace is specified, it will automatically use `minecraft:`. +In that case you should keep track of the bossbar with the id that `bossbar(id)` returns, because a namespace may be added automatically. +If the id was invalid (for example by having more than one colon), returns `null`. +If the bossbar already exists, returns `false`. + +`bossbar('timer') => 'minecraft:timer'` (Adds the namespace `minecraft:` because none is specified) + +`bossbar('scarpet:test') => 'scarpet:test'` In this case there is already a namespace specified + +`bossbar('foo:bar:baz') => null` Invalid identifier + +`bossbar(id,property)` is used to query the `property` of a bossbar. + +`bossbar(id,property,value)` can modify the `property` of the bossbar to a specified `value`. + +Available properties are: + +* color: can be `'pink'`, `'blue'`, `'red'`, `'green'`, `'yellow'`, `'purple'` or `'white'` + +* style: can be `'progress'`, `'notched_6'`, `'notched_10'`, `'notched_12'` or `'notched_20'` + +* value: value of the bossbar progress + +* max: maximum value of the bossbar progress, by default 100 + +* name: Text to display above the bossbar, supports formatted text + +* visible: whether the bossbar is visible or not + +* players: List of players that can see the bossbar + +* add_player: add a player to the players that can see this bossbar, this can only be used for modifying (`value` must be present) + +* remove: remove this bossbar, no `value` required + +``` +bossbar('script:test','style','notched_12') +bossbar('script:test','value',74) +bossbar('script:test','name',format('rb Test')) -> Change text +bossbar('script:test','visible',false) -> removes visibility, but keeps players +bossbar('script:test','players',player('all')) -> Visible for all players +bossbar('script:test','players',player('Steve')) -> Visible for Steve only +bossbar('script:test','players',null) -> Invalid player, removing all players +bossbar('script:test','add_player',player('Alex')) -> Add Alex to the list of players that can see the bossbar +bossbar('script:test','remove') -> remove bossbar 'script:test' +for(bossbar(),bossbar(_,'remove')) -> remove all bossbars +``` + + + + + diff --git a/docs/scarpet/api/ScriptCommand.md b/docs/scarpet/api/ScriptCommand.md index b10b5dc6..f2f2aa6a 100644 --- a/docs/scarpet/api/ScriptCommand.md +++ b/docs/scarpet/api/ScriptCommand.md @@ -8,13 +8,15 @@ to simulate running commands in a different scope. # `/script load / unload (global?)`, `/script in ` commands -`load / unload` commands allow for very conventient way of writing your code, providing it to the game and -distribute with your worlds without the need of use of commandblocks. Just place your scarpet code in the -/scripts folder of your world files and make sure it ends with `.sc` extension. The good thing about editing that -code is that you can no only use normal editing without the need of marking of newlines, +`load / unload` commands allow for very convenient way of writing your code, providing it to the game and +distribute with your worlds without the need of use of commandblocks. Just place your Scarpet code in the +`/scripts` folder of your world files and make sure it ends with `.sc` extension. In singleplayer, you can +also save your scripts in `.minecraft/config/carpet/scripts` to make them available in any world. + +The good thing about editing that code is that you can not only use normal editing without the need of marking of newlines, but you can also use comments in your code. -a comment is anything that starts with a double slash, and continues to the end of the line: +A comment is anything that starts with a double slash, and continues to the end of the line:
 foo = 1;
diff --git a/docs/scarpet/language/Containers.md b/docs/scarpet/language/Containers.md
index 1f4759d9..8b07497c 100644
--- a/docs/scarpet/language/Containers.md
+++ b/docs/scarpet/language/Containers.md
@@ -215,6 +215,8 @@ map(range(10),_*_)  => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
 reduce(range(10),_a+_, 0)  => 45
 range(5,10)  => [5, 6, 7, 8, 9]
 range(20, 10, -2)  => [20, 18, 16, 14, 12]
+range(-0.3, 0.3, 0.1)  => [-0.3, -0.2, -0.1, 0, 0.1, 0.2]
+range(0.3, -0.3, -0.1) => [0.3, 0.2, 0.1, -0, -0.1, -0.2]
 
## Map operations diff --git a/docs/scarpet/language/FunctionsAndControlFlow.md b/docs/scarpet/language/FunctionsAndControlFlow.md index 2d6a8d00..3d11e8bf 100644 --- a/docs/scarpet/language/FunctionsAndControlFlow.md +++ b/docs/scarpet/language/FunctionsAndControlFlow.md @@ -135,7 +135,7 @@ foo(... x) -> ... # all arguments for foo are included in the list ### `import(module_name, symbols ...)` Imports symbols from other apps and libraries into the current one: global variables or functions, allowing to use -them in the current app. This include other symbols imported by these modules. Scarpet supports cicular dependencies, +them in the current app. This includes other symbols imported by these modules. Scarpet supports circular dependencies, but if symbols are used directly in the module body rather than functions, it may not be able to retrieve them. Returns full list of available symbols that could be imported from this module, which can be used to debug import issues, and list contents of libraries. @@ -144,9 +144,57 @@ issues, and list contents of libraries. calls a user defined function with specified arguments. It is equivalent to calling `function(args...)` directly except you can use it with function value, or name instead. This means you can pass functions to other user defined -functions as arguments and call them with `call` internally. And since function definitions return the defined +functions as arguments and call them with `call` internally. Since function definitions return the defined function, they can be defined in place as anonymous functions. +#### Passing function references to other modules of your application + +In case a function is defined by its name, Scarpet will attempt to resolve its definition in the given module and its imports, +meaning if the call is in a imported library, and not in the main module of your app, and that function is not visible from the +library perspective, but in the app, it won't be call-able. In case you pass a function name to a separate module in your app, +it should import back that method from the main module for visibility. + +Check an example of a problematic code of a library that expects a function value as a passed argument and how it is called in +the parent app: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme('foo' , 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` + +In this case `'foo'` will fail to dereference in `lib` as it is not visible by name. In tightly coupled modules, where `lib` is just +a component of your `app` you can use circular import to acknowledge the symbol from the other module (pretty much like +imports in Java classes), and that solves the issue but makes the library dependent on the main app: +``` +//lib.scl +import('app','foo'); +callme(fun, arg) -> call(fun, arg); +``` +You can circumvent that issue by explicitly dereferencing the local function where it is used as a lambda argument created +in the module in which the requested function is visible: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme(_(x) -> foo(x), 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` +Or by passing an explicit reference to the function, instead of calling it by name: +``` +//app.sc +import('lib', 'callme'); +global_foohandler = (foo(x) -> x*x); +test() -> callme(global_foohandler, 5); +``` + Little technical note: the use of `_` in expression passed to built in functions is much more efficient due to not creating new call stacks for each invoked function, but anonymous functions is the only mechanism available for programmers with their own lambda arguments diff --git a/docs/scarpet/language/Overview.md b/docs/scarpet/language/Overview.md index 64c3d81d..0030ad73 100644 --- a/docs/scarpet/language/Overview.md +++ b/docs/scarpet/language/Overview.md @@ -151,9 +151,10 @@ have memory and act like objects so to speak. Check `outer(var)` for details. ## Code delivery, line indicators Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing -your code in apps (files with `.sc` extension that can be placed inside "/scripts" folder in the world files and -loaded as a scarpet app with command `/script load [app_name]`. Scarpet apps loaded from disk should only -contain code, no need to start with "/script run" prefix +your code in apps (files with `.sc` extension that can be placed inside `/scripts` folder in the world files +or as a globally available app in singleplayer in the `.minecraft/config/carpet/scripts` folder and loaded +as a Scarpet app with the command `/script load [app_name]`. Scarpet apps loaded from disk should only +contain code, no need to start with `/script run` prefix. The following is the code that could be provided in a `foo.sc` app file located in world `/scripts` folder diff --git a/docs/scarpet/language/SystemFunctions.md b/docs/scarpet/language/SystemFunctions.md index 0959f243..1f3768e6 100644 --- a/docs/scarpet/language/SystemFunctions.md +++ b/docs/scarpet/language/SystemFunctions.md @@ -122,6 +122,9 @@ a = 3; task_thread('temp', _(outer(a), b) -> foo(a,b), 5) => Another example of running the same thing passing arguments using closure over anonymous function as well as passing a parameter.
+In case you want to create a task based on a function that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + ### `sleep()` `sleep(timeout)`, `sleep(timeout, close_expr)` diff --git a/docs/scarpet/resources/editors/idea/2.txt b/docs/scarpet/resources/editors/idea/2.txt index 642a8bc5..25f4e954 100644 --- a/docs/scarpet/resources/editors/idea/2.txt +++ b/docs/scarpet/resources/editors/idea/2.txt @@ -12,8 +12,10 @@ block_tick block_tags blocks_daylight blocks_movement +bossbar brightness chunk_tickets +convert_date crafting_remaining_item create_marker current_dimension @@ -51,9 +53,12 @@ inventory_remove inventory_set inventory_size item_category +item_list +item_tags last_tick_times light liquid +list_files load_app_data loaded loaded_status @@ -92,10 +97,10 @@ scoreboard_remove scoreboard_display tag_matches team_add -team_list -team_remove team_leave +team_list team_property +team_remove read_file see_sky set @@ -121,8 +126,9 @@ ticks_randomly top transparent update +unix_time view_distance volume without_updates world_time -write_file \ No newline at end of file +write_file diff --git a/docs/scarpet/resources/editors/npp/scarpet.xml b/docs/scarpet/resources/editors/npp/scarpet.xml index 65a7800d..81706a3d 100644 --- a/docs/scarpet/resources/editors/npp/scarpet.xml +++ b/docs/scarpet/resources/editors/npp/scarpet.xml @@ -58,15 +58,19 @@ structure_eligibility structures structure_references set_structure reset_chunk reload_chunk custom_dimension add_chunk_ticket + convert_date unix_time + display_title scoreboard scoreboard_add scoreboard_remove scoreboard_display team_add team_list team_remove team_leave team_property + bossbar + player spawn entity_id entity_list entity_area entity_selector query modify entity_event entity_load_handler entity_types - stack_limit item_category recipe_data crafting_remaining_item + item_list item_tags stack_limit item_category recipe_data crafting_remaining_item inventory_size inventory_has_items inventory_get inventory_set inventory_find inventory_remove drop_item @@ -80,7 +84,7 @@ plop harvest destroy schedule nbt draw_shape create_marker remove_all_markers - load_app_data store_app_data read_file write_file delete_file + load_app_data store_app_data read_file write_file delete_file list_files escape_nbt parse_nbt encode_nbt tag_matches statistic euler pi null true false diff --git a/gradle.properties b/gradle.properties index 8caaf1a1..9551a36d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ org.gradle.jvmargs=-Xmx1G loader_version=0.10.8 # Mod Properties - mod_version = 1.4.19+v201125 + mod_version = 1.4.21+v201216 maven_group = carpet archives_base_name = fabric-carpet diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java index 4b94b3fa..773b61e3 100644 --- a/src/main/java/carpet/CarpetServer.java +++ b/src/main/java/carpet/CarpetServer.java @@ -5,7 +5,19 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; -import carpet.commands.*; +import carpet.commands.CounterCommand; +import carpet.commands.DistanceCommand; +import carpet.commands.DrawCommand; +import carpet.commands.InfoCommand; +import carpet.commands.LogCommand; +import carpet.commands.MobAICommand; +import carpet.commands.PerimeterInfoCommand; +import carpet.commands.PlayerCommand; +import carpet.commands.ProfileCommand; +import carpet.commands.ScriptCommand; +import carpet.commands.SpawnCommand; +import carpet.commands.TestCommand; +import carpet.commands.TickCommand; import carpet.network.ServerNetworkHandler; import carpet.helpers.HopperCounter; import carpet.helpers.TickSpeed; @@ -90,9 +102,9 @@ public class CarpetServer implements ClientModInitializer,DedicatedServerModInit public static void onServerLoadedWorlds(MinecraftServer minecraftServer) { + HopperCounter.resetAll(minecraftServer); extensions.forEach(e -> e.onServerLoadedWorlds(minecraftServer)); scriptServer.initializeForWorld(); - HopperCounter.resetAll(minecraftServer); } public static void tick(MinecraftServer server) @@ -143,6 +155,7 @@ public class CarpetServer implements ClientModInitializer,DedicatedServerModInit { ServerNetworkHandler.onPlayerJoin(player); LoggerRegistry.playerConnected(player); + scriptServer.onPlayerJoin(player); extensions.forEach(e -> e.onPlayerLoggedIn(player)); } @@ -156,9 +169,9 @@ public class CarpetServer implements ClientModInitializer,DedicatedServerModInit public static void onServerClosed(MinecraftServer server) { + if (scriptServer != null) scriptServer.onClose(); ServerNetworkHandler.close(); currentCommandDispatcher = null; - if (scriptServer != null) scriptServer.onClose(); LoggerRegistry.stopLoggers(); extensions.forEach(e -> e.onServerClosed(server)); diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java index 4a72fe1b..172a3cb8 100644 --- a/src/main/java/carpet/CarpetSettings.java +++ b/src/main/java/carpet/CarpetSettings.java @@ -36,7 +36,7 @@ import static carpet.settings.RuleCategory.CLIENT; @SuppressWarnings("CanBeFinal") public class CarpetSettings { - public static final String carpetVersion = "1.4.19+v201125"; + public static final String carpetVersion = "1.4.21+v201216"; public static final Logger LOG = LogManager.getLogger("carpet"); public static boolean skipGenerationChecks = false; public static boolean impendingFillSkipUpdates = false; @@ -427,7 +427,7 @@ public class CarpetSettings extra = "if /script is enabled", category = SCARPET ) - public static boolean scriptsAutoload = false; + public static boolean scriptsAutoload = true; @Rule(desc = "Enables /player command to control/spawn players", category = COMMAND) public static String commandPlayer = "ops"; diff --git a/src/main/java/carpet/commands/PlayerCommand.java b/src/main/java/carpet/commands/PlayerCommand.java index 0414acb4..9aa9297b 100644 --- a/src/main/java/carpet/commands/PlayerCommand.java +++ b/src/main/java/carpet/commands/PlayerCommand.java @@ -58,6 +58,9 @@ public class PlayerCommand .then(makeActionCommand("dropStack", EntityPlayerActionPack.ActionType.DROP_STACK)) .then(makeDropCommand("dropStack", true)) .then(makeActionCommand("swapHands", EntityPlayerActionPack.ActionType.SWAP_HANDS)) + .then(literal("hotbar") + .then(argument("slot", IntegerArgumentType.integer(1, 9)) + .executes(c -> manipulate(c, ap -> ap.setSlot(IntegerArgumentType.getInteger(c, "slot")))))) .then(literal("kill").executes(PlayerCommand::kill)) .then(literal("shadow"). executes(PlayerCommand::shadow)) .then(literal("mount").executes(manipulation(ap -> ap.mount(true))) diff --git a/src/main/java/carpet/commands/ScriptCommand.java b/src/main/java/carpet/commands/ScriptCommand.java index 29973381..75030545 100644 --- a/src/main/java/carpet/commands/ScriptCommand.java +++ b/src/main/java/carpet/commands/ScriptCommand.java @@ -18,6 +18,7 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.minecraft.block.Block; @@ -84,7 +85,7 @@ public class ScriptCommand private static CompletableFuture suggestCode( CommandContext context, SuggestionsBuilder suggestionsBuilder - ) + ) throws CommandSyntaxException { CarpetScriptHost currentHost = getHost(context); String previous = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT); @@ -291,7 +292,7 @@ public class ScriptCommand )?1:0)))))). then(literal("remove_from"). then(argument("event", StringArgumentType.word()). - suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.publicEvents(CarpetServer.scriptServer).stream().map(ev -> ev.name).collect(Collectors.toList()) ,bb)). + suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.publicEvents(CarpetServer.scriptServer).stream().filter(CarpetEventServer.Event::isNeeded).map(ev -> ev.name).collect(Collectors.toList()) ,bb)). then(argument("call", StringArgumentType.greedyString()). suggests( (cc, bb) -> suggestMatching(CarpetEventServer.Event.getEvent(StringArgumentType.getString(cc, "event"), CarpetServer.scriptServer).handler.callList.stream().map(CarpetEventServer.Callback::toString), bb)). executes( (cc) -> CarpetServer.scriptServer.events.removeEventFromCommand( @@ -311,14 +312,14 @@ public class ScriptCommand suggests( (cc, bb) -> suggestMatching(CarpetServer.scriptServer.modules.keySet(), bb)). then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t)))); } - private static CarpetScriptHost getHost(CommandContext context) + private static CarpetScriptHost getHost(CommandContext context) throws CommandSyntaxException { CarpetScriptHost host; try { String name = StringArgumentType.getString(context, "app").toLowerCase(Locale.ROOT); CarpetScriptHost parentHost = CarpetServer.scriptServer.modules.getOrDefault(name, CarpetServer.scriptServer.globalHost); - host = parentHost.retrieveForExecution(context.getSource()); + host = parentHost.retrieveOwnForExecution(context.getSource()); } catch (IllegalArgumentException ignored) { @@ -327,7 +328,7 @@ public class ScriptCommand host.setChatErrorSnooper(context.getSource()); return host; } - private static Collection suggestFunctionCalls(CommandContext c) + private static Collection suggestFunctionCalls(CommandContext c) throws CommandSyntaxException { CarpetScriptHost host = getHost(c); return host.globaFunctionNames(host.main, s -> !s.startsWith("_")).sorted().collect(Collectors.toList()); @@ -350,7 +351,7 @@ public class ScriptCommand } return 1; } - private static int listGlobals(CommandContext context, boolean all) + private static int listGlobals(CommandContext context, boolean all) throws CommandSyntaxException { CarpetScriptHost host = getHost(context); ServerCommandSource source = context.getSource(); @@ -425,7 +426,7 @@ public class ScriptCommand //host.resetErrorSnooper(); // lets say no need to reset the snooper in case something happens on the way } - private static int invoke(CommandContext context, String call, BlockPos pos1, BlockPos pos2, String args) + private static int invoke(CommandContext context, String call, BlockPos pos1, BlockPos pos2, String args) throws CommandSyntaxException { ServerCommandSource source = context.getSource(); CarpetScriptHost host = getHost(context); @@ -453,7 +454,7 @@ public class ScriptCommand } - private static int compute(CommandContext context, String expr) + private static int compute(CommandContext context, String expr) throws CommandSyntaxException { ServerCommandSource source = context.getSource(); CarpetScriptHost host = getHost(context); @@ -463,7 +464,7 @@ public class ScriptCommand }); } - private static int scriptScan(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr) + private static int scriptScan(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr) throws CommandSyntaxException { ServerCommandSource source = context.getSource(); CarpetScriptHost host = getHost(context); @@ -512,7 +513,7 @@ public class ScriptCommand private static int scriptFill(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr, - BlockStateArgument block, Predicate replacement, String mode) + BlockStateArgument block, Predicate replacement, String mode) throws CommandSyntaxException { ServerCommandSource source = context.getSource(); CarpetScriptHost host = getHost(context); diff --git a/src/main/java/carpet/fakes/HungerManagerInterface.java b/src/main/java/carpet/fakes/HungerManagerInterface.java index b4df2334..f6a2787c 100644 --- a/src/main/java/carpet/fakes/HungerManagerInterface.java +++ b/src/main/java/carpet/fakes/HungerManagerInterface.java @@ -3,4 +3,5 @@ package carpet.fakes; public interface HungerManagerInterface { float getExhaustionCM(); void setExhaustionCM(float exhaust); + void setSaturationCM(float saturn); } diff --git a/src/main/java/carpet/fakes/ServerWorldInterface.java b/src/main/java/carpet/fakes/ServerWorldInterface.java new file mode 100644 index 00000000..67414706 --- /dev/null +++ b/src/main/java/carpet/fakes/ServerWorldInterface.java @@ -0,0 +1,7 @@ +package carpet.fakes; + +import net.minecraft.world.level.ServerWorldProperties; + +public interface ServerWorldInterface { + ServerWorldProperties getWorldPropertiesCM(); +} diff --git a/src/main/java/carpet/helpers/EntityPlayerActionPack.java b/src/main/java/carpet/helpers/EntityPlayerActionPack.java index 92220866..0c1d8253 100644 --- a/src/main/java/carpet/helpers/EntityPlayerActionPack.java +++ b/src/main/java/carpet/helpers/EntityPlayerActionPack.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.vehicle.BoatEntity; import net.minecraft.entity.vehicle.MinecartEntity; import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.s2c.play.HeldItemChangeS2CPacket; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; import net.minecraft.server.world.ServerWorld; @@ -276,6 +277,12 @@ public class EntityPlayerActionPack } } + public void setSlot(int slot) + { + player.inventory.selectedSlot = slot-1; + player.networkHandler.sendPacket(new HeldItemChangeS2CPacket(slot-1)); + } + public enum ActionType { USE(true) diff --git a/src/main/java/carpet/helpers/OptimizedExplosion.java b/src/main/java/carpet/helpers/OptimizedExplosion.java index ddaaa92d..3130f30a 100644 --- a/src/main/java/carpet/helpers/OptimizedExplosion.java +++ b/src/main/java/carpet/helpers/OptimizedExplosion.java @@ -132,7 +132,8 @@ public class OptimizedExplosion if (d12 <= 1.0D) { double d5 = entity.getX() - eAccess.getX(); - double d7 = entity.getY() + (double) entity.getStandingEyeHeight() - eAccess.getY(); + // Change in 1.16 snapshots to fix a bug with TNT jumping + double d7 = (entity instanceof TntEntity ? entity.getY() : entity.getEyeY()) - eAccess.getY(); double d9 = entity.getZ() - eAccess.getZ(); double d13 = (double) MathHelper.sqrt(d5 * d5 + d7 * d7 + d9 * d9); diff --git a/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java b/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java index 3e6fcdc2..2f841f1c 100644 --- a/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java +++ b/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java @@ -8,13 +8,11 @@ import org.spongepowered.asm.mixin.Shadow; @Mixin(HungerManager.class) public class HungerManagerMixin_scarpetEventsMixin implements HungerManagerInterface { @Shadow private float exhaustion; - public float getExhaustionCM(){ - return exhaustion; - } - + @Shadow private float foodSaturationLevel; @Override - public void setExhaustionCM(float exhaust) - { - exhaustion = exhaust; - } + public float getExhaustionCM(){ return exhaustion; } + @Override + public void setExhaustionCM(float exhaust) { exhaustion = exhaust; } + @Override + public void setSaturationCM(float saturn) { foodSaturationLevel = saturn; } } diff --git a/src/main/java/carpet/mixins/PlayerManager_fakePlayersMixin.java b/src/main/java/carpet/mixins/PlayerManager_fakePlayersMixin.java index faee9ef4..1b5f6be8 100644 --- a/src/main/java/carpet/mixins/PlayerManager_fakePlayersMixin.java +++ b/src/main/java/carpet/mixins/PlayerManager_fakePlayersMixin.java @@ -69,7 +69,7 @@ public abstract class PlayerManager_fakePlayersMixin ServerPlayerEntity serverPlayerEntity_3 = (ServerPlayerEntity) var5.next(); if(serverPlayerEntity_3 instanceof EntityPlayerMPFake) { - serverPlayerEntity_3.kill(); + ((EntityPlayerMPFake)serverPlayerEntity_3).kill(new TranslatableText("multiplayer.disconnect.duplicate_login")); continue; } serverPlayerEntity_3.networkHandler.disconnect(new TranslatableText("multiplayer.disconnect.duplicate_login")); diff --git a/src/main/java/carpet/mixins/ServerWorld_scarpetEventMixin.java b/src/main/java/carpet/mixins/ServerWorld_scarpetMixin.java similarity index 86% rename from src/main/java/carpet/mixins/ServerWorld_scarpetEventMixin.java rename to src/main/java/carpet/mixins/ServerWorld_scarpetMixin.java index 5c11338a..5e83e881 100644 --- a/src/main/java/carpet/mixins/ServerWorld_scarpetEventMixin.java +++ b/src/main/java/carpet/mixins/ServerWorld_scarpetMixin.java @@ -1,6 +1,7 @@ package carpet.mixins; import carpet.CarpetSettings; +import carpet.fakes.ServerWorldInterface; import carpet.script.CarpetEventServer; import net.minecraft.entity.Entity; import net.minecraft.entity.LightningEntity; @@ -9,7 +10,9 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.ChunkPos; import net.minecraft.util.profiler.Profiler; import net.minecraft.world.chunk.WorldChunk; +import net.minecraft.world.level.ServerWorldProperties; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -19,7 +22,7 @@ import static carpet.script.CarpetEventServer.Event.ENTITY_LOAD; import static carpet.script.CarpetEventServer.Event.LIGHTNING; @Mixin(ServerWorld.class) -public class ServerWorld_scarpetEventMixin +public class ServerWorld_scarpetMixin implements ServerWorldInterface { @Inject(method = "tickChunk", locals = LocalCapture.CAPTURE_FAILHARD, at = @At( value = "INVOKE", @@ -54,4 +57,9 @@ public class ServerWorld_scarpetEventMixin } } + @Shadow + private ServerWorldProperties worldProperties; + public ServerWorldProperties getWorldPropertiesCM(){ + return worldProperties; + } } diff --git a/src/main/java/carpet/mixins/TradeOutputSlot_scarpetEventMixin.java b/src/main/java/carpet/mixins/TradeOutputSlot_scarpetEventMixin.java new file mode 100644 index 00000000..053e799b --- /dev/null +++ b/src/main/java/carpet/mixins/TradeOutputSlot_scarpetEventMixin.java @@ -0,0 +1,34 @@ +package carpet.mixins; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.TradeOutputSlot; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.village.Merchant; +import net.minecraft.village.MerchantInventory; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import static carpet.script.CarpetEventServer.Event.PLAYER_TRADES; + +@Mixin(value = TradeOutputSlot.class) +public abstract class TradeOutputSlot_scarpetEventMixin { + @Shadow @Final private Merchant merchant; + + @Shadow @Final private MerchantInventory merchantInventory; + + @Inject(method = "onTakeItem", at = @At(value = "INVOKE", + target = "Lnet/minecraft/village/Merchant;trade(Lnet/minecraft/village/TradeOffer;)V") + ) + private void onTrade(PlayerEntity player, ItemStack stack, CallbackInfoReturnable cir) { + if(PLAYER_TRADES.isNeeded() && !this.merchant.getMerchantWorld().isClient()) + { + PLAYER_TRADES.onTrade((ServerPlayerEntity) player, merchant, merchantInventory.getTradeOffer()); + } + } +} diff --git a/src/main/java/carpet/network/ServerNetworkHandler.java b/src/main/java/carpet/network/ServerNetworkHandler.java index 5971e93d..72ffc14f 100644 --- a/src/main/java/carpet/network/ServerNetworkHandler.java +++ b/src/main/java/carpet/network/ServerNetworkHandler.java @@ -217,6 +217,13 @@ public class ServerNetworkHandler } + public static String getPlayerStatus(ServerPlayerEntity player) + { + if (remoteCarpetPlayers.containsKey(player)) return "carpet "+remoteCarpetPlayers.get(player); + if (validCarpetPlayers.contains(player)) return "carpet "+CarpetSettings.carpetVersion; + return "vanilla"; + } + private static class DataBuilder { private CompoundTag tag; diff --git a/src/main/java/carpet/patches/EntityPlayerMPFake.java b/src/main/java/carpet/patches/EntityPlayerMPFake.java index 357d28c8..e7932250 100644 --- a/src/main/java/carpet/patches/EntityPlayerMPFake.java +++ b/src/main/java/carpet/patches/EntityPlayerMPFake.java @@ -9,6 +9,7 @@ import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket; import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.player.HungerManager; import net.minecraft.network.NetworkSide; +import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ServerTask; @@ -95,9 +96,14 @@ public class EntityPlayerMPFake extends ServerPlayerEntity @Override public void kill() + { + kill(Messenger.s("Killed")); + } + + public void kill(Text reason) { this.server.send(new ServerTask(this.server.getTicks(), () -> { - this.networkHandler.onDisconnected(Messenger.s("Killed")); + this.networkHandler.onDisconnected(reason); })); } @@ -120,6 +126,12 @@ public class EntityPlayerMPFake extends ServerPlayerEntity super.onDeath(cause); setHealth(20); this.hungerManager = new HungerManager(); - kill(); + kill(this.getDamageTracker().getDeathMessage()); + } + + @Override + public String getIp() + { + return "127.0.0.1"; } } diff --git a/src/main/java/carpet/patches/NetHandlerPlayServerFake.java b/src/main/java/carpet/patches/NetHandlerPlayServerFake.java index af85ff30..330cb54d 100644 --- a/src/main/java/carpet/patches/NetHandlerPlayServerFake.java +++ b/src/main/java/carpet/patches/NetHandlerPlayServerFake.java @@ -6,6 +6,7 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayNetworkHandler; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; public class NetHandlerPlayServerFake extends ServerPlayNetworkHandler { @@ -22,6 +23,10 @@ public class NetHandlerPlayServerFake extends ServerPlayNetworkHandler @Override public void disconnect(Text message) { + if (player instanceof EntityPlayerMPFake && message instanceof TranslatableText && ((TranslatableText) message).getKey().equals("multiplayer.disconnect.idling")) + { + ((EntityPlayerMPFake) player).kill(new TranslatableText(((TranslatableText) message).getKey())); + } } } diff --git a/src/main/java/carpet/script/CarpetEventServer.java b/src/main/java/carpet/script/CarpetEventServer.java index f734c070..4b6b31b6 100644 --- a/src/main/java/carpet/script/CarpetEventServer.java +++ b/src/main/java/carpet/script/CarpetEventServer.java @@ -14,9 +14,11 @@ import carpet.script.value.Value; import carpet.script.value.ValueConversions; import carpet.settings.ParsedRule; import carpet.utils.Messenger; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.block.BlockState; import net.minecraft.entity.EntityType; import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.passive.MerchantEntity; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.entity.Entity; @@ -34,6 +36,8 @@ import net.minecraft.util.math.Direction; import net.minecraft.util.math.Vec3d; import net.minecraft.util.registry.Registry; import net.minecraft.util.registry.RegistryKey; +import net.minecraft.village.Merchant; +import net.minecraft.village.TradeOffer; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import org.apache.commons.lang3.tuple.Pair; @@ -47,8 +51,9 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; -import java.util.function.Function; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -65,59 +70,84 @@ public class CarpetEventServer public static class Callback { public final String host; + public final String optionalTarget; public final FunctionValue function; public final List parametrizedArgs; - public Callback(String host, FunctionValue function, List parametrizedArgs) + public Callback(String host, String target, FunctionValue function, List parametrizedArgs) { this.host = host; this.function = function; + this.optionalTarget = target; this.parametrizedArgs = parametrizedArgs==null?NOARGS:parametrizedArgs; } /** * Used also in entity events - * @param asSource - entity command source + * @param sender - entity command source * @param runtimeArgs = options */ - public boolean execute(ServerCommandSource asSource, List runtimeArgs) + public boolean execute(ServerCommandSource sender, List runtimeArgs) { - if (this.parametrizedArgs.isEmpty()) - return CarpetServer.scriptServer.runas( - asSource.withLevel(CarpetSettings.runPermissionLevel), - host, function, runtimeArgs); - else + if (!this.parametrizedArgs.isEmpty()) { - List combinedArgs = new ArrayList<>(runtimeArgs); - combinedArgs.addAll(this.parametrizedArgs); - return CarpetServer.scriptServer.runas( - asSource.withLevel(CarpetSettings.runPermissionLevel), - host, function, combinedArgs); + runtimeArgs = new ArrayList<>(runtimeArgs); + runtimeArgs.addAll(this.parametrizedArgs); } + return CarpetServer.scriptServer.runEventCall( + sender.withLevel(CarpetSettings.runPermissionLevel), + host, optionalTarget, function, runtimeArgs + ); } /** * Used also in entity events - * @param recipient - optional target player argument + * @param sender - sender of the signal + * @param optionalRecipient - optional target player argument * @param runtimeArgs = options */ - public Boolean targetPlayer(ServerPlayerEntity recipient, List runtimeArgs) + public int signal(ServerCommandSource sender, ServerPlayerEntity optionalRecipient, List runtimeArgs) { + // recipent of the call doesn't match the handlingHost + if (optionalRecipient != null && !optionalRecipient.getEntityName().equals(optionalTarget)) + return 0; List args = runtimeArgs; if (!this.parametrizedArgs.isEmpty()) { args = new ArrayList<>(runtimeArgs); args.addAll(this.parametrizedArgs); } - return CarpetServer.scriptServer.runForPlayer(recipient, host, function, args); + return CarpetServer.scriptServer.signal(sender, optionalRecipient, host, function, args, false); } @Override public String toString() { - return function.getString()+((host==null)?"":"(from "+host+")"); + return function.getString()+((host==null)?"":"(from "+host+(optionalTarget == null?"":"/"+optionalTarget)+")"); + } + public static class Signature + { + String function; + String host; + String target; + public Signature(String fun, String h, String t) + { + function = fun; + host = h; + target = t; + } + } + public static Signature fromString(String str) + { + Pattern find = Pattern.compile("(\\w+)(?:\\(from (\\w+)(?:/(\\w+))?\\))?"); + Matcher matcher = find.matcher(str); + if(matcher.matches()) + { + return new Signature(matcher.group(1), matcher.group(2), matcher.group(3)); + } + return new Signature(str, null, null); } } @@ -129,7 +159,8 @@ public class CarpetEventServer public ScheduledCall(CarpetContext context, FunctionValue function, List args, long dueTime) { - super(context.host.getName(), function, args); + // ignoring target as we will be always calling self + super(context.host.getName(), null, function, args); this.ctx = context; this.dueTime = dueTime; } @@ -139,7 +170,7 @@ public class CarpetEventServer */ public void execute() { - CarpetServer.scriptServer.runas(ctx.origin, ctx.s, host, function, parametrizedArgs); + CarpetServer.scriptServer.runScheduledCall(ctx.origin, ctx.s, host, (CarpetScriptHost) ctx.host, function, parametrizedArgs); } @@ -173,7 +204,7 @@ public class CarpetEventServer } } - public int callCustom(ServerPlayerEntity receipient, List callArg) + public int signal(ServerCommandSource sender, ServerPlayerEntity optinoalReceipient, List callArg) { if (callList.isEmpty()) return 0; //List fails = new ArrayList<>(); @@ -181,15 +212,13 @@ public class CarpetEventServer int successes = 0; for (Callback call: callList) { - Boolean result = call.targetPlayer(receipient, callArg); - //if (result == Boolean.FALSE) fails.add(call); - if (result == Boolean.TRUE) successes++; + successes += Math.max(0, call.signal(sender, optinoalReceipient, callArg)); } //for (Callback call : fails) callList.remove(call); return successes; } - public boolean addEventCall(ServerCommandSource source, String hostName, String funName, Function verifier) + public boolean addFromExternal(ServerCommandSource source, String hostName, String funName, Consumer hostOnEventHandler) { ScriptHost host = CarpetServer.scriptServer.getHostByName(hostName); if (host == null) @@ -198,11 +227,7 @@ public class CarpetEventServer Messenger.m(source, "r Unknown app "+hostName); return false; } - if (!verifier.apply(host)) - { - Messenger.m(source, "r Global event can only be added to apps with global scope"); - return false; - } + hostOnEventHandler.accept(host); FunctionValue udf = host.getFunction(funName); if (udf == null || udf.getArguments().size() != reqArgs) { @@ -210,11 +235,24 @@ public class CarpetEventServer Messenger.m(source, "r Callback doesn't expect required number of arguments: "+reqArgs); return false; } + String target = null; + if (host.isPerUser()) + { + try + { + target = source.getPlayer().getEntityName(); + } + catch (CommandSyntaxException e) + { + Messenger.m(source, "r Cannot add event to a player scoped app from a command without a player context"); + return false; + } + } //all clear //remove duplicates - removeEventCall(hostName, udf.getString()); - callList.add(new Callback(hostName, udf, null)); + removeEventCall(hostName, target, udf.getString()); + callList.add(new Callback(hostName, target, udf, null)); return true; } public boolean addEventCallInternal(ScriptHost host, FunctionValue function, List args) @@ -225,20 +263,39 @@ public class CarpetEventServer } //all clear //remove duplicates - removeEventCall(host.getName(), function.getString()); - callList.add(new Callback(host.getName(), function, args)); + removeEventCall(host.getName(), host.user, function.getString()); + callList.add(new Callback(host.getName(), host.user, function, args)); return true; } - public void removeEventCall(String hostName, String funName) + public void removeEventCall(String hostName, String target, String funName) { - callList.removeIf((c)-> c.function.getString().equals(funName) && ( hostName == c.host || (c.host != null && c.host.equalsIgnoreCase(hostName) )) ); + callList.removeIf((c)-> c.function.getString().equals(funName) + && (Objects.equals(c.host, hostName)) + && (Objects.equals(c.optionalTarget, target)) + ); } public void removeAllCalls(CarpetScriptHost host) { - callList.removeIf((c)-> c.host != null && c.host.equals(host.getName())); + callList.removeIf((c)-> (Objects.equals(c.host, host.getName())) + && (Objects.equals(c.optionalTarget, host.user))); } + + public void createChildEvents(CarpetScriptHost host) + { + List copyCalls = new ArrayList<>(); + callList.forEach((c)-> + { + if ((Objects.equals(c.host, host.getName())) + && c.optionalTarget == null) + { + copyCalls.add(new Callback(c.host, host.user, c.function, c.parametrizedArgs)); + } + }); + callList.addAll(copyCalls); + } + public void clearEverything() { callList.clear(); @@ -255,6 +312,30 @@ public class CarpetEventServer return events; } + public static final Event START = new Event("server_starts", 0, true) + { + @Override + public void onTick() + { + handler.call(Collections::emptyList, () -> + CarpetServer.minecraft_server.getCommandSource(). + withWorld(CarpetServer.minecraft_server.getWorld(World.OVERWORLD)) + ); + } + }; + + public static final Event SHUTDOWN = new Event("server_shuts_down", 0, true) + { + @Override + public void onTick() + { + handler.call(Collections::emptyList, () -> + CarpetServer.minecraft_server.getCommandSource(). + withWorld(CarpetServer.minecraft_server.getWorld(World.OVERWORLD)) + ); + } + }; + public static final Event TICK = new Event("tick", 0, true) { @Override @@ -470,6 +551,20 @@ public class CarpetEventServer ), player::getCommandSource); } }; + public static final Event PLAYER_TRADES = new Event("player_trades", 5, false) + { + @Override + public void onTrade(ServerPlayerEntity player, Merchant merchant, TradeOffer tradeOffer) + { + handler.call( () -> Arrays.asList( + new EntityValue(player), + merchant instanceof MerchantEntity ? new EntityValue((MerchantEntity) merchant) : Value.NULL, + ValueConversions.of(tradeOffer.getOriginalFirstBuyItem()), + ValueConversions.of(tradeOffer.getSecondBuyItem()), + ValueConversions.of(tradeOffer.getSellItem()) + ), player::getCommandSource); + } + }; public static final Event PLAYER_PICKS_UP_ITEM = new Event("player_picks_up_item", 2, false) { @Override @@ -801,6 +896,12 @@ public class CarpetEventServer host.getScriptServer().events.customEvents.values().forEach((e) -> e.handler.removeAllCalls(host)); } + public static void transferAllHostEventsToChild(CarpetScriptHost host) + { + byName.values().forEach((e) -> e.handler.createChildEvents(host)); + host.getScriptServer().events.customEvents.values().forEach((e) -> e.handler.createChildEvents(host)); + } + public static void clearAllBuiltinEvents() { byName.values().forEach(e -> e.handler.clearEverything()); @@ -842,6 +943,7 @@ public class CarpetEventServer public void onDamage(Entity target, float amount, DamageSource source) { } public void onRecipeSelected(ServerPlayerEntity player, Identifier recipe, boolean fullStack) {} public void onSlotSwitch(ServerPlayerEntity player, int from, int to) {} + public void onTrade(ServerPlayerEntity player, Merchant merchant, TradeOffer tradeOffer) {} public void onWorldEvent(ServerWorld world, BlockPos pos) { } @@ -888,7 +990,7 @@ public class CarpetEventServer { return false; } - boolean added = ev.handler.addEventCall(source, host, funName, h -> canAddEvent(ev, h)); + boolean added = ev.handler.addFromExternal(source, host, funName, h -> onEventAddedToHost(ev, h)); if (added) Messenger.m(source, "gi Added " + funName + " to " + event); return added; } @@ -897,8 +999,7 @@ public class CarpetEventServer { // this is globals only Event ev = Event.byName.get(event); - if (!canAddEvent(ev, host)) - throw new InternalExpressionException("Global event "+event+" can only be added to apps with global scope"); + onEventAddedToHost(ev, host); boolean success = ev.handler.addEventCallInternal(host, function, args==null?NOARGS:args); if (!success) throw new InternalExpressionException("Global event "+event+" requires "+ev.handler.reqArgs+", not "+(function.getNumParams()-((args==null)?0:args.size()))); } @@ -906,22 +1007,21 @@ public class CarpetEventServer public boolean handleCustomEvent(String event, CarpetScriptHost host, FunctionValue function, List args) { Event ev = Event.getOrCreateCustom(event, host.getScriptServer()); - if (!canAddEvent(ev, host)) - throw new InternalExpressionException("Global built-in event "+event+" can only be handled in apps with global scope"); + onEventAddedToHost(ev, host); return ev.handler.addEventCallInternal(host, function, args==null?NOARGS:args); } - public int signalEvent(String event, CarpetScriptHost host, ServerPlayerEntity player, List callArgs) + public int signalEvent(String event, CarpetContext cc, ServerPlayerEntity optionalTarget, List callArgs) { - Event ev = Event.getEvent(event, host.getScriptServer()); + Event ev = Event.getEvent(event, ((CarpetScriptHost)cc.host).getScriptServer()); if (ev == null) return -1; - return ev.handler.callCustom(player, callArgs); + return ev.handler.signal(cc.s, optionalTarget, callArgs); } - private boolean canAddEvent(Event event, ScriptHost host) + private void onEventAddedToHost(Event event, ScriptHost host) { if (event.deprecated()) host.issueDeprecation(event.name+" event"); - return !(event.globalOnly && (host.perUser || host.parent != null)); + //return !(event.globalOnly && (host.perUser || host.parent != null)); } public boolean removeEventFromCommand(ServerCommandSource source, String event, String funName) @@ -932,8 +1032,8 @@ public class CarpetEventServer Messenger.m(source, "r Unknown event: " + event); return false; } - Pair call = decodeCallback(funName); - ev.handler.removeEventCall(call.getLeft(), call.getRight()); + Callback.Signature call = Callback.fromString(funName); + ev.handler.removeEventCall(call.host, call.target, call.function); // could verified if actually removed Messenger.m(source, "gi Removed event: " + funName + " from "+event); return true; @@ -949,13 +1049,15 @@ public class CarpetEventServer public void removeBuiltInEvent(String event, CarpetScriptHost host, String funName) { Event ev = Event.getEvent(event, host.getScriptServer()); - if (ev != null) ev.handler.removeEventCall(host.getName(), funName); + if (ev != null) ev.handler.removeEventCall(host.getName(), host.user, funName); } public void removeAllHostEvents(CarpetScriptHost host) { // remove event handlers Event.removeAllHostEvents(host); + if (host.isPerUser()) + for (ScriptHost child: host.userHosts.values()) Event.removeAllHostEvents((CarpetScriptHost) child); // remove scheduled calls scheduledCalls.removeIf(sc -> sc.host != null && sc.host.equals(host.getName())); } diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java index 5981a863..09e08153 100644 --- a/src/main/java/carpet/script/CarpetScriptHost.java +++ b/src/main/java/carpet/script/CarpetScriptHost.java @@ -1,6 +1,7 @@ package carpet.script; import carpet.CarpetServer; +import carpet.CarpetSettings; import carpet.script.api.Auxiliary; import carpet.script.argument.FunctionArgument; import carpet.script.bundled.Module; @@ -10,12 +11,12 @@ import carpet.script.exception.CarpetExpressionException; import carpet.script.exception.ExpressionException; import carpet.script.exception.InternalExpressionException; import carpet.script.exception.InvalidCallbackException; +import carpet.script.value.EntityValue; import carpet.script.value.FunctionValue; import carpet.script.value.MapValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; import carpet.script.value.Value; -import carpet.CarpetSettings; import carpet.utils.Messenger; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; @@ -38,9 +39,11 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static java.lang.Math.max; import static net.minecraft.server.command.CommandManager.literal; @@ -117,7 +120,7 @@ public class CarpetScriptHost extends ScriptHost private static int execute(CommandContext ctx, String hostName, FunctionArgument funcSpec, List paramNames) throws CommandSyntaxException { - CarpetScriptHost cHost = CarpetServer.scriptServer.modules.get(hostName).retrieveForExecution(ctx.getSource()); + CarpetScriptHost cHost = CarpetServer.scriptServer.modules.get(hostName).retrieveOwnForExecution(ctx.getSource()); List argNames = funcSpec.function.getArguments(); if ((argNames.size()-funcSpec.args.size()) != paramNames.size()) throw new SimpleCommandExceptionType(new LiteralText("Target function "+funcSpec.function.getPrettyString()+" as wrong number of arguments, required "+paramNames.size()+", found "+argNames.size()+" with "+funcSpec.args.size()+" provided")).create(); @@ -162,8 +165,9 @@ public class CarpetScriptHost extends ScriptHost ) throws CommandSyntaxException { String hostName = main.getName(); + Function configValidator = getCommandConfigPermissions(); LiteralArgumentBuilder command = literal(hostName). - requires((player) -> CarpetServer.scriptServer.modules.containsKey(hostName) && useValidator.apply(player)); + requires((player) -> CarpetServer.scriptServer.modules.containsKey(hostName) && useValidator.apply(player) && configValidator.apply(player)); for (Pair,FunctionArgument> commandData : entries) { command = this.addPathToCommand(command, commandData.getKey(), commandData.getValue()); @@ -171,12 +175,63 @@ public class CarpetScriptHost extends ScriptHost return command; } + public Function getCommandConfigPermissions() throws CommandSyntaxException + { + Value confValue = appConfig.get(StringValue.of("command_permission")); + if (confValue == null) return s -> true; + if (confValue instanceof NumericValue) + { + int level = ((NumericValue) confValue).getInt(); + if (level < 1 || level > 4) throw CommandArgument.error("Numeric permission level for custom commands should be between 1 and 4"); + return s -> s.hasPermissionLevel(level); + } + if (!(confValue instanceof FunctionValue)) + { + String perm = confValue.getString().toLowerCase(Locale.ROOT); + switch (perm) + { + case "ops": return s -> s.hasPermissionLevel(2); + case "server": return s -> !(s.getEntity() instanceof ServerPlayerEntity); + case "players": return s -> s.getEntity() instanceof ServerPlayerEntity; + case "all": return s -> true; + default: throw CommandArgument.error("Unknown command permission: "+perm); + } + } + FunctionValue fun = (FunctionValue) confValue; + if (fun.getNumParams() != 1) throw CommandArgument.error("Custom command permission function should expect 1 argument"); + String hostName = getName(); + return s -> { + try + { + CarpetScriptHost cHost = null; + cHost = CarpetServer.scriptServer.modules.get(hostName).retrieveOwnForExecution(s); + Value response = cHost.handleCommand(s, fun, Collections.singletonList( + (s.getEntity() instanceof ServerPlayerEntity)?new EntityValue(s.getEntity()):Value.NULL) + ); + return response.getBoolean(); + } + catch (CommandSyntaxException e) + { + Messenger.m(s, "rb Unable to run app command: "+e.getMessage()); + return false; + } + }; + } + @Override protected ScriptHost duplicate() { return new CarpetScriptHost(scriptServer, main, false, this, appConfig, appArgTypes, commandValidator, isRuleApp); } + @Override + protected void transferToChild(ScriptHost host) + { + super.transferToChild(host); + // transfer Events + CarpetEventServer.Event.transferAllHostEventsToChild((CarpetScriptHost) host); + } + @Override public void addUserDefinedFunction(Context ctx, Module module, String funName, FunctionValue function) { @@ -208,7 +263,7 @@ public class CarpetScriptHost extends ScriptHost if (!(ret instanceof MapValue)) return false; Map config = ((MapValue) ret).getMap(); setPerPlayer(config.getOrDefault(new StringValue("scope"), new StringValue("player")).getString().equalsIgnoreCase("player")); - persistenceRequired = config.getOrDefault(new StringValue("stay_loaded"), Value.FALSE).getBoolean(); + persistenceRequired = config.getOrDefault(new StringValue("stay_loaded"), Value.TRUE).getBoolean(); // read custom arguments Value arguments = config.get(StringValue.of("arguments")); if (arguments != null) @@ -319,38 +374,58 @@ public class CarpetScriptHost extends ScriptHost } } - public CarpetScriptHost retrieveForExecution(ServerCommandSource source) + public List retrieveForExecution(ServerCommandSource source, String optionalTarget) { - CarpetScriptHost host = this; + List targets = new ArrayList<>(); if (perUser) { - try + if (optionalTarget == null) { - ServerPlayerEntity player = source.getPlayer(); - host = (CarpetScriptHost) retrieveForExecution(player.getName().getString()); + for (ServerPlayerEntity player : source.getMinecraftServer().getPlayerManager().getPlayerList()) + { + CarpetScriptHost host = (CarpetScriptHost) retrieveForExecution(player.getEntityName()); + targets.add(host); + if (host.errorSnooper == null) host.setChatErrorSnooper(player.getCommandSource()); + } } - catch (CommandSyntaxException e) + else { - host = (CarpetScriptHost) retrieveForExecution((String) null); + ServerPlayerEntity player = source.getMinecraftServer().getPlayerManager().getPlayer(optionalTarget); + if (player != null) + { + CarpetScriptHost host = (CarpetScriptHost) retrieveForExecution(player.getEntityName()); + targets.add(host); + if (host.errorSnooper == null) host.setChatErrorSnooper(player.getCommandSource()); + } } } - if (host.errorSnooper == null) host.setChatErrorSnooper(source); - return host; + else + { + targets.add(this); + if (this.errorSnooper == null) this.setChatErrorSnooper(source); + } + return targets; } - public CarpetScriptHost retrieveForExecution(ServerPlayerEntity target) + public CarpetScriptHost retrieveOwnForExecution(ServerCommandSource source) throws CommandSyntaxException { - // allowing only run - if (perUser == (target == null)) return null; - if (!perUser) { - if (errorSnooper == null) setChatErrorSnooper(this.scriptServer.server.getCommandSource()); + if (errorSnooper == null) setChatErrorSnooper(source); return this; } // user based - CarpetScriptHost userHost = (CarpetScriptHost)retrieveForExecution(target.getName().getString()); - if (userHost.errorSnooper == null) userHost.setChatErrorSnooper(target.getCommandSource()); + ServerPlayerEntity player; + try + { + player = source.getPlayer(); + } + catch (CommandSyntaxException ignored) + { + throw new SimpleCommandExceptionType(new LiteralText("Cannot run player based apps without the player context")).create(); + } + CarpetScriptHost userHost = (CarpetScriptHost)retrieveForExecution(player.getEntityName()); + if (userHost.errorSnooper == null) userHost.setChatErrorSnooper(source); return userHost; } @@ -645,6 +720,12 @@ public class CarpetScriptHost extends ScriptHost return Module.listFile(main, resource, "txt", isShared); } + public Stream listFolder(String resource, String ext, boolean isShared) + { + if (getName() == null && !isShared) return null; // + return Module.listFolder(main, resource, ext, isShared); + } + public void tick() { diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java index 69fd0327..7f0a3f2c 100644 --- a/src/main/java/carpet/script/CarpetScriptServer.java +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -1,8 +1,8 @@ package carpet.script; +import carpet.CarpetSettings; import carpet.script.argument.FunctionArgument; import carpet.script.bundled.BundledModule; -import carpet.CarpetSettings; import carpet.CarpetServer; import carpet.script.bundled.FileModule; import carpet.script.bundled.Module; @@ -16,19 +16,25 @@ import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.CommandNode; + +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.server.MinecraftServer; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.WorldSavePath; import net.minecraft.util.math.BlockPos; -import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -116,31 +122,42 @@ public class CarpetScriptServer }); if (CarpetSettings.scriptsAutoload) { - Messenger.m(server.getCommandSource(), "Auto-loading world scarpet apps"); for (String moduleName: listAvailableModules(false)) { addScriptHost(server.getCommandSource(), moduleName, null, true, true, false); } } - + CarpetEventServer.Event.START.onTick(); } public Module getModule(String name, boolean allowLibraries) { - File folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts").toFile(); - File[] listOfFiles = folder.listFiles(); - if (listOfFiles != null) - for (File script : listOfFiles) + try { + Path folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts"); + Files.createDirectories(folder); + Optional + scriptPath = Files.list(folder) + .filter(script -> + script.getFileName().toString().equalsIgnoreCase(name+".sc") || + (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name+".scl")) + ).findFirst(); + if (scriptPath.isPresent()) + return new FileModule(scriptPath.get()); + + if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) { - if (script.getName().equalsIgnoreCase(name+".sc")) - { - return new FileModule(script); - } - if (allowLibraries && script.getName().equalsIgnoreCase(name+".scl")) - { - return new FileModule(script); - } + Path globalFolder = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts"); + Files.createDirectories(globalFolder); + scriptPath = Files.walk(globalFolder) + .filter(script -> script.getFileName().toString().equalsIgnoreCase(name + ".sc") || + (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl"))) + .findFirst(); + if (scriptPath.isPresent()) + return new FileModule(scriptPath.get()); } + } catch (IOException e) { + CarpetSettings.LOG.error("Exception while loading the app: ", e); + } for (Module moduleData : bundledModuleData) { if (moduleData.getName().equalsIgnoreCase(name) && (allowLibraries || !moduleData.isLibrary())) @@ -173,17 +190,23 @@ public class CarpetScriptServer if (!mi.isLibrary() && !mi.getName().endsWith("_beta")) moduleNames.add(mi.getName()); } } - File folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts").toFile(); - File[] listOfFiles = folder.listFiles(); - if (listOfFiles == null) - return moduleNames; - for (File script : listOfFiles) - { - if (script.getName().endsWith(".sc")) + try { + Path worldScripts = server.getSavePath(WorldSavePath.ROOT).resolve("scripts"); + Files.createDirectories(worldScripts); + Files.list(worldScripts) + .filter(f -> f.toString().endsWith(".sc")) + .forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT))); + + if (includeBuiltIns && (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)) { - String name = script.getName().replaceFirst("\\.sc","").toLowerCase(Locale.ROOT); - moduleNames.add(name); + Path globalScripts = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts"); + Files.createDirectories(globalScripts); + Files.walk(globalScripts) + .filter(f -> f.toString().endsWith(".sc")) + .forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT))); } + } catch (IOException e) { + CarpetSettings.LOG.error("Exception while searching for apps: ", e); } return moduleNames; } @@ -198,7 +221,6 @@ public class CarpetScriptServer public boolean addScriptHost(ServerCommandSource source, String name, Function commandValidator, boolean perPlayer, boolean autoload, boolean isRuleApp) { - //TODO add per player modules to support player actions better on a server if (commandValidator == null) commandValidator = p -> true; long start = System.nanoTime(); name = name.toLowerCase(Locale.ROOT); @@ -272,6 +294,11 @@ public class CarpetScriptServer Messenger.m(source, "r Failed to build command system for "+name+" thus failed to load the app"); return false; } + if (newHost.isPerUser()) + { + // that will provide player hosts right at the startup + newHost.retrieveForExecution(source, null); + } long end = System.nanoTime(); CarpetSettings.LOG.info("App "+name+" loaded in "+(end-start)/1000000+" ms"); return true; @@ -297,12 +324,23 @@ public class CarpetScriptServer return true; } + Function configValidator; + try + { + configValidator = host.getCommandConfigPermissions(); + } + catch (CommandSyntaxException e) + { + Messenger.m(source, "rb "+e.getMessage()); + return false; + } + LiteralArgumentBuilder command = literal(hostName). - requires((player) -> modules.containsKey(hostName) && useValidator.apply(player)). + requires((player) -> modules.containsKey(hostName) && useValidator.apply(player) && configValidator.apply(player)). executes( (c) -> { - Value response = modules.get(hostName).retrieveForExecution(c.getSource()). - handleCommandLegacy(c.getSource(),"__command", null, ""); + CarpetScriptHost targetHost = modules.get(hostName).retrieveOwnForExecution(c.getSource()); + Value response = targetHost.handleCommandLegacy(c.getSource(),"__command", null, ""); if (!response.isNull()) Messenger.m(c.getSource(), "gi "+response.getString()); return (int)response.readInteger(); }); @@ -331,15 +369,15 @@ public class CarpetScriptServer then(literal(function). requires((player) -> modules.containsKey(hostName) && modules.get(hostName).getFunction(function) != null). executes((c) -> { - Value response = modules.get(hostName).retrieveForExecution(c.getSource()). - handleCommandLegacy(c.getSource(), function, null, ""); + CarpetScriptHost targetHost = modules.get(hostName).retrieveOwnForExecution(c.getSource()); + Value response = targetHost.handleCommandLegacy(c.getSource(),function, null, ""); if (!response.isNull()) Messenger.m(c.getSource(), "gi " + response.getString()); return (int) response.readInteger(); }). then(argument("args...", StringArgumentType.greedyString()). executes( (c) -> { - Value response = modules.get(hostName).retrieveForExecution(c.getSource()). - handleCommandLegacy(c.getSource(), function,null, StringArgumentType.getString(c, "args...")); + CarpetScriptHost targetHost = modules.get(hostName).retrieveOwnForExecution(c.getSource()); + Value response = targetHost.handleCommandLegacy(c.getSource(),function, null, StringArgumentType.getString(c, "args...")); if (!response.isNull()) Messenger.m(c.getSource(), "gi "+response.getString()); return (int)response.readInteger(); }))); @@ -370,54 +408,84 @@ public class CarpetScriptServer return true; } - public boolean runas(ServerCommandSource source, String hostname, FunctionValue udf, List argv) - { - return runas(BlockPos.ORIGIN, source, hostname, udf, argv); - } - - public boolean runas(BlockPos origin, ServerCommandSource source, String hostname, FunctionValue udf, List argv) + public boolean runEventCall(ServerCommandSource sender, String hostname, String optionalTarget, FunctionValue udf, List argv) { CarpetScriptHost host = globalHost; - try + if (hostname != null) + host = modules.get(hostname); + if (host == null) return false; + // dummy call for player apps that reside on the global copy - do not run them, but report as successes. + if (host.isPerUser() && optionalTarget==null) return true; + ServerPlayerEntity target = null; + if (optionalTarget != null) { - if (hostname != null) - host = modules.get(hostname).retrieveForExecution(source); - host.callUDF(origin, source, udf, argv); + target = sender.getMinecraftServer().getPlayerManager().getPlayer(optionalTarget); + if (target == null) return false; } - catch (NullPointerException | InvalidCallbackException npe) - { - return false; - } - return true; + int successes = signal(sender, target, hostname, udf, argv, true ); + return successes >= 0; } - public Boolean runForPlayer(ServerPlayerEntity recipient, String hostname, FunctionValue udf, List argv) + public void runScheduledCall(BlockPos origin, ServerCommandSource source, String hostname, CarpetScriptHost host, FunctionValue udf, List argv) + { + if (hostname != null && !modules.containsKey(hostname)) // well - scheduled call app got unloaded + return; + try + { + host.callUDF(origin, source, udf, argv); + } + catch (NullPointerException | InvalidCallbackException ignored) + { + } + } + + /** + * returns number of successful calls in the host or -1 if failed exceptionally + * @param sender + * @param optionalRecipient + * @param hostname + * @param udf + * @param argv + * @param reportFails - returns -1 when any call failed + * @return + */ + public int signal(ServerCommandSource sender, ServerPlayerEntity optionalRecipient, String hostname, FunctionValue udf, List argv, boolean reportFails) { - CarpetScriptHost host; - ServerCommandSource source; if (hostname == null) { - if (recipient != null) return null; - host = globalHost; - source = server.getCommandSource(); + if (optionalRecipient != null) return 0; + try + { + globalHost.callUDF(BlockPos.ORIGIN, sender, udf, argv); + } + catch (NullPointerException | InvalidCallbackException npe) + { + return reportFails?-1:0; + } + return 1; } - else + String hostRecipient = optionalRecipient == null?null:optionalRecipient.getEntityName(); + ServerCommandSource source = (optionalRecipient == null)? sender : optionalRecipient.getCommandSource().withLevel(CarpetSettings.runPermissionLevel); + int successes = 0; + for (CarpetScriptHost host : modules.get(hostname).retrieveForExecution(sender, hostRecipient)) { - host = modules.get(hostname).retrieveForExecution(recipient); - if (host == null) // not applicable - return null; - source = (recipient == null)? server.getCommandSource() : recipient.getCommandSource(); + // getPlayer will always return nonnull cause retrieve for execution only returns hosts for existing players. + ServerCommandSource executingSource = host.perUser + ? source.getMinecraftServer().getPlayerManager().getPlayer(host.user).getCommandSource() + : source.getMinecraftServer().getCommandSource(); + try + { + host.callUDF(BlockPos.ORIGIN, source.withLevel(CarpetSettings.runPermissionLevel), udf, argv); + } + catch (NullPointerException | InvalidCallbackException npe) + { + if (reportFails) return -1; + continue; + } + successes ++; } - try - { - host.callUDF(BlockPos.ORIGIN, source.withLevel(CarpetSettings.runPermissionLevel), udf, argv); - } - catch (NullPointerException | InvalidCallbackException npe) - { - return false; - } - return true; + return successes; } public void tick() @@ -431,12 +499,30 @@ public class CarpetScriptServer public void onClose() { + CarpetEventServer.Event.SHUTDOWN.onTick(); for (ScriptHost host : modules.values()) { host.onClose(); } } + public void onPlayerJoin(ServerPlayerEntity player) + { + modules.values().forEach(h -> + { + if (h.isPerUser()) + { + try + { + h.retrieveOwnForExecution(player.getCommandSource()); + } + catch (CommandSyntaxException ignored) + { + } + } + }); + } + static class TransferData { boolean perUser; diff --git a/src/main/java/carpet/script/EntityEventsGroup.java b/src/main/java/carpet/script/EntityEventsGroup.java index 76d09f33..3fd3f0b0 100644 --- a/src/main/java/carpet/script/EntityEventsGroup.java +++ b/src/main/java/carpet/script/EntityEventsGroup.java @@ -9,6 +9,7 @@ import carpet.script.value.StringValue; import carpet.script.value.Value; import net.minecraft.entity.Entity; import net.minecraft.entity.damage.DamageSource; +import org.apache.commons.lang3.tuple.Pair; import java.util.Arrays; import java.util.Collections; @@ -19,7 +20,7 @@ import java.util.Map; public class EntityEventsGroup { - private final Map> actions; + private final Map, CarpetEventServer.Callback>> actions; private final Entity entity; public EntityEventsGroup(Entity e) { @@ -30,37 +31,47 @@ public class EntityEventsGroup public void onEvent(Event type, Object... args) { if (actions.isEmpty()) return; // most of the cases, trying to be nice - Map actionSet = actions.get(type); + Map, CarpetEventServer.Callback> actionSet = actions.get(type); if (actionSet == null) return; - for (Iterator> iterator = actionSet.entrySet().iterator(); iterator.hasNext(); ) + for (Iterator, CarpetEventServer.Callback>> iterator = actionSet.entrySet().iterator(); iterator.hasNext(); ) { - Map.Entry action = iterator.next(); - ScriptHost host = CarpetServer.scriptServer.getHostByName(action.getKey()); + Map.Entry, CarpetEventServer.Callback> action = iterator.next(); + Pair key = action.getKey(); + ScriptHost host = CarpetServer.scriptServer.getHostByName(key.getLeft()); if (host == null) { iterator.remove(); continue; } + if (key.getRight() != null) + { + if (entity.getServer().getPlayerManager().getPlayer(key.getRight())==null) + { + iterator.remove(); + continue; + } + } if (!type.call(action.getValue(), entity, args)) iterator.remove(); } if (actionSet.isEmpty()) actions.remove(type); } - public void addEvent(Event type, String hostName, FunctionValue fun, List extraargs) + public void addEvent(Event type, ScriptHost host, FunctionValue fun, List extraargs) { + Pair key = Pair.of(host.getName(), host.user); if (fun != null) { - CarpetEventServer.Callback call = type.create(hostName, fun, extraargs); + CarpetEventServer.Callback call = type.create(key, fun, extraargs); if (call == null) throw new InternalExpressionException("wrong number of arguments for callback, required "+type.argcount); - actions.computeIfAbsent(type, k -> new HashMap<>()).put(hostName, call); + actions.computeIfAbsent(type, k -> new HashMap<>()).put(key, call); } else { - actions.computeIfAbsent(type, k -> new HashMap<>()).remove(hostName); + actions.computeIfAbsent(type, k -> new HashMap<>()).remove(key); if (actions.get(type).isEmpty()) actions.remove(type); } @@ -107,13 +118,13 @@ public class EntityEventsGroup argcount = args+1; // entity is not extra byName.put(identifier, this); } - public CarpetEventServer.Callback create(String hostName, FunctionValue function, List extraArgs) + public CarpetEventServer.Callback create(Pair key, FunctionValue function, List extraArgs) { if ((function.getArguments().size()-(extraArgs == null ? 0 : extraArgs.size())) != argcount) { return null; } - return new CarpetEventServer.Callback(hostName, function, extraArgs); + return new CarpetEventServer.Callback(key.getLeft(), key.getRight(), function, extraArgs); } public boolean call(CarpetEventServer.Callback tickCall, Entity entity, Object ... args) { diff --git a/src/main/java/carpet/script/ScriptHost.java b/src/main/java/carpet/script/ScriptHost.java index c55861a6..d984673f 100644 --- a/src/main/java/carpet/script/ScriptHost.java +++ b/src/main/java/carpet/script/ScriptHost.java @@ -18,13 +18,10 @@ import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.function.Predicate; -import java.util.stream.Collectors; import java.util.stream.Stream; public abstract class ScriptHost @@ -189,7 +186,13 @@ public abstract class ScriptHost public FunctionValue getAssertFunction(Module module, String name) { FunctionValue ret = getFunction(module, name); - if (ret == null) throw new InternalExpressionException("Function "+name+" is not defined yet"); + if (ret == null) + { + if (module == main) + throw new InternalExpressionException("Function '"+name+"' is not defined yet"); + else + throw new InternalExpressionException("Function '"+name+"' is not defined nor visible by its name in the imported module '"+module.getName()+"'"); + } return ret; } private FunctionValue getFunction(Module module, String name) @@ -318,15 +321,21 @@ public abstract class ScriptHost ScriptHost oldUserHost = userHosts.get(user); if (oldUserHost != null) return oldUserHost; ScriptHost userHost = this.duplicate(); - userHost.modules.putAll(this.modules); - this.moduleData.forEach((key, value) -> userHost.moduleData.put(key, new ModuleData(key, value))); - // fixing imports - userHost.moduleData.forEach((module, data) -> data.setImportsBasedOn(userHost, this.moduleData.get(data.parent))); userHost.user = user; + this.transferToChild(userHost); userHosts.put(user, userHost); return userHost; } + protected void transferToChild(ScriptHost host) + { + // adding imports + host.modules.putAll(this.modules); + this.moduleData.forEach((key, value) -> host.moduleData.put(key, new ModuleData(key, value))); + // fixing imports + host.moduleData.forEach((module, data) -> data.setImportsBasedOn(host, this.moduleData.get(data.parent))); + } + public void handleExpressionException(String msg, ExpressionException exc) { System.out.println(msg+": "+exc); diff --git a/src/main/java/carpet/script/api/Auxiliary.java b/src/main/java/carpet/script/api/Auxiliary.java index a835fc5c..eeefd681 100644 --- a/src/main/java/carpet/script/api/Auxiliary.java +++ b/src/main/java/carpet/script/api/Auxiliary.java @@ -18,6 +18,7 @@ import carpet.script.argument.FunctionArgument; import carpet.script.argument.Vector3Argument; import carpet.script.exception.ExitStatement; import carpet.script.exception.InternalExpressionException; +import carpet.script.utils.FixedCommandSource; import carpet.script.utils.ShapeDispatcher; import carpet.script.utils.WorldTools; import carpet.script.value.EntityValue; @@ -32,8 +33,8 @@ import carpet.script.value.StringValue; import carpet.script.value.Value; import carpet.script.value.ValueConversions; import carpet.utils.Messenger; +import com.google.common.collect.ImmutableMap; import net.minecraft.block.BlockState; -import net.minecraft.command.argument.EntityAnchorArgumentType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; import net.minecraft.entity.EquipmentSlot; @@ -48,9 +49,7 @@ import net.minecraft.network.packet.s2c.play.TitleS2CPacket; import net.minecraft.network.packet.s2c.play.TitleS2CPacket.Action; import net.minecraft.particle.ParticleEffect; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.command.CommandOutput; import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.server.network.ServerPlayNetworkHandler; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.sound.SoundCategory; @@ -63,12 +62,12 @@ import net.minecraft.util.InvalidIdentifierException; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.ChunkPos; import net.minecraft.util.math.EulerAngle; -import net.minecraft.util.math.Vec2f; import net.minecraft.util.math.Vec3d; import net.minecraft.util.registry.DynamicRegistryManager; import net.minecraft.util.registry.Registry; import net.minecraft.world.World; import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; import java.util.ArrayList; import java.util.Arrays; @@ -87,19 +86,39 @@ import static java.lang.Math.min; public class Auxiliary { public static final String MARKER_STRING = "__scarpet_marker"; private static final Map mixerMap = Arrays.stream(SoundCategory.values()).collect(Collectors.toMap(SoundCategory::getName, k -> k)); + private static final Map supportedTypes = ImmutableMap.of( + "raw", ".txt", + "text", ".txt", + "nbt", ".nbt", + "folder", "folder" + ); - public static String recognizeResource(Value value) + public static String recognizeResource(Value value, boolean isFloder) { String origfile = value.getString(); String file = origfile.toLowerCase(Locale.ROOT).replaceAll("[^A-Za-z0-9\\-+_/]", ""); file = Arrays.stream(file.split("/+")).filter(s -> !s.isEmpty()).collect(Collectors.joining("/")); - if (file.isEmpty()) + if (file.isEmpty() && !isFloder) { throw new InternalExpressionException("Cannot use "+origfile+" as resource name - must have some letters and numbers"); } return file; } + public static Triple getFileDescriptor(List lv, Context c, boolean isFloder) + { + if (lv.size() < 2) throw new InternalExpressionException("File functions require path and type as first two arguments"); + String resource = recognizeResource(lv.get(0).evalValue(c), isFloder); + String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); + boolean shared = origtype.startsWith("shared_"); + String type = shared ? origtype.substring(7) : origtype; //len(shared_) + if (!supportedTypes.containsKey(type)) + throw new InternalExpressionException("Unsupported file type: "+origtype); + if (type.equals("folder") && !isFloder) + throw new InternalExpressionException("Folder types are no supported for this IO function"); + return Triple.of(resource, type, shared); + } + public static void apply(Expression expression) { expression.addLazyFunction("sound", -1, (c, t, lv) -> { @@ -310,7 +329,7 @@ public class Auxiliary { { Value nameValue = lv.get(0).evalValue(c); name = nameValue instanceof NullValue ? "" : nameValue.getString(); - pointLocator = Vector3Argument.findIn(cc, lv, 1, true); + pointLocator = Vector3Argument.findIn(cc, lv, 1, true, false); if (lv.size()>pointLocator.offset) { BlockArgument blockLocator = BlockArgument.findIn(cc, lv, pointLocator.offset, true, true, false); @@ -446,28 +465,29 @@ public class Auxiliary { { if (lv.size() == 0 || lv.size() > 2) throw new InternalExpressionException("'print' takes one or two arguments"); ServerCommandSource s = ((CarpetContext)c).s; + MinecraftServer server = s.getMinecraftServer(); Value res = lv.get(0).evalValue(c); + List targets = null; if (lv.size() == 2) { - ServerPlayerEntity player = EntityValue.getPlayerByValue(s.getMinecraftServer(), res); - if (player == null) return LazyValue.NULL; - s = player.getCommandSource(); + List playerValues = (res instanceof ListValue)?((ListValue) res).getItems():Collections.singletonList(res); + List playerTargets = new ArrayList<>(); + playerValues.forEach(pv -> { + ServerPlayerEntity player = EntityValue.getPlayerByValue(server, pv); + if (player == null) throw new InternalExpressionException("Cannot target player "+pv.getString()+" in print"); + playerTargets.add(player); + }); + targets = playerTargets; res = lv.get(1).evalValue(c); } - if (res instanceof FormattedTextValue) + Text message = (res instanceof FormattedTextValue)?((FormattedTextValue) res).getText():new LiteralText(res.getString()); + if (targets == null) { - s.sendFeedback(((FormattedTextValue) res).getText(), false); + s.sendFeedback(message, false); } else { - if (s.getEntity() instanceof PlayerEntity) - { - Messenger.m((PlayerEntity) s.getEntity(), "w " + res.getString()); - } - else - { - Messenger.m(s, "w " + res.getString()); - } + targets.forEach(p -> p.getCommandSource().sendFeedback(message, false)); } Value finalRes = res; return (_c, _t) -> finalRes; // pass through for variables @@ -510,7 +530,7 @@ public class Auxiliary { if (pVal instanceof FormattedTextValue) title = ((FormattedTextValue) pVal).getText(); else - title = Text.of(pVal.getString()); + title = new LiteralText(pVal.getString()); } TitleS2CPacket timesPacket; if (lv.size() > 3) @@ -544,29 +564,14 @@ public class Auxiliary { expression.addLazyFunction("run", 1, (c, t, lv) -> { BlockPos target = ((CarpetContext)c).origin; - Vec3d posf = new Vec3d((double)target.getX()+0.5D,(double)target.getY(),(double)target.getZ()+0.5D); + Vec3d posf = new Vec3d((double)target.getX()+0.5D, target.getY(),(double)target.getZ()+0.5D); ServerCommandSource s = ((CarpetContext)c).s; try { Value[] error = {Value.NULL}; List output = new ArrayList<>(); Value retval = new NumericValue(s.getMinecraftServer().getCommandManager().execute( - new ServerCommandSource( - CommandOutput.DUMMY, posf, Vec2f.ZERO, s.getWorld(), CarpetSettings.runPermissionLevel, - s.getName(), s.getDisplayName(), s.getMinecraftServer(), s.getEntity(), true, - (ctx, succ, res) -> { }, EntityAnchorArgumentType.EntityAnchor.FEET) - { - @Override - public void sendError(Text message) - { - error[0] = new FormattedTextValue(message); - } - @Override - public void sendFeedback(Text message, boolean broadcastToOps) - { - output.add(new FormattedTextValue(message)); - } - }, + new FixedCommandSource(s, posf, error, output), lv.get(0).evalValue(c).getString()) ); Value ret = ListValue.of(retval, ListValue.wrap(output), error[0]); @@ -785,23 +790,29 @@ public class Auxiliary { return (_c, _t) -> res; // pass through for variables }); - expression.addLazyFunction("read_file", 2, (c, t, lv) -> { - String resource = recognizeResource(lv.get(0).evalValue(c)); - String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); - boolean shared = origtype.startsWith("shared_"); - String type = shared ? origtype.substring(7) : origtype; //len(shared_) - if (!type.equals("raw") && !type.equals("text") && !type.equals("nbt")) - throw new InternalExpressionException("Unsupported file type: "+origtype); + expression.addLazyFunction("list_files", 2, (c, t, lv) -> + { + Triple fdesc = getFileDescriptor(lv, c, true); + Stream files = ((CarpetScriptHost) c.host).listFolder(fdesc.getLeft(), supportedTypes.get(fdesc.getMiddle()), fdesc.getRight()); + if (files == null) return LazyValue.NULL; + Value ret = ListValue.wrap(files.map(StringValue::of).collect(Collectors.toList())); + return (cc, tt) -> ret; + }); + + + expression.addLazyFunction("read_file", 2, (c, t, lv) -> + { + Triple fdesc = getFileDescriptor(lv, c, false); Value retVal; - if (type.equals("nbt")) + if (fdesc.getMiddle().equals("nbt")) { - Tag state = ((CarpetScriptHost)((CarpetContext)c).host).readFileTag(resource, shared); + Tag state = ((CarpetScriptHost) c.host).readFileTag(fdesc.getLeft(), fdesc.getRight()); if (state == null) return LazyValue.NULL; retVal = new NBTSerializableValue(state); } else { - List content = ((CarpetScriptHost) ((CarpetContext) c).host).readTextResource(resource, shared); + List content = ((CarpetScriptHost) c.host).readTextResource(fdesc.getLeft(), fdesc.getRight()); if (content == null) return LazyValue.NULL; retVal = ListValue.wrap(content.stream().map(StringValue::new).collect(Collectors.toList())); } @@ -809,33 +820,26 @@ public class Auxiliary { }); expression.addLazyFunction("delete_file", 2, (c, t, lv) -> { - String resource = recognizeResource(lv.get(0).evalValue(c)); - String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); - boolean shared = origtype.startsWith("shared_"); - String type = shared ? origtype.substring(7) : origtype; //len(shared_) - if (!type.equals("raw") && !type.equals("text") && !type.equals("nbt")) - throw new InternalExpressionException("Unsupported file type: "+origtype); - boolean success = ((CarpetScriptHost)((CarpetContext)c).host).removeResourceFile(resource, shared, type); - return success?LazyValue.TRUE:LazyValue.FALSE; + Triple fdesc = getFileDescriptor(lv, c, false); + return ((CarpetScriptHost) c.host).removeResourceFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle()) + ? LazyValue.TRUE + : LazyValue.FALSE; }); expression.addLazyFunction("write_file", -1, (c, t, lv) -> { if (lv.size() < 3) throw new InternalExpressionException("'write_file' requires three or more arguments"); - String resource = recognizeResource(lv.get(0).evalValue(c)); - String origtype = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); - boolean shared = origtype.startsWith("shared_"); - String type = shared ? origtype.substring(7) : origtype; //len(shared_) - if (!type.equals("raw") && !type.equals("text") && !type.equals("nbt")) - throw new InternalExpressionException("Unsupported file type: "+origtype); + Triple fdesc = getFileDescriptor(lv, c, false); + + boolean success; - if (type.equals("nbt")) + if (fdesc.getMiddle().equals("nbt")) { Value val = lv.get(2).evalValue(c); NBTSerializableValue tagValue = (val instanceof NBTSerializableValue) ? (NBTSerializableValue) val : new NBTSerializableValue(val.getString()); Tag tag = tagValue.getTag(); - success = ((CarpetScriptHost)((CarpetContext)c).host).writeTagFile(tag, resource, shared); + success = ((CarpetScriptHost) c.host).writeTagFile(tag, fdesc.getLeft(), fdesc.getRight()); } else { @@ -860,7 +864,7 @@ public class Auxiliary { data.add(lv.get(i).evalValue(c).getString()); } } - success = ((CarpetScriptHost) ((CarpetContext) c).host).appendLogFile(resource, shared, type, data); + success = ((CarpetScriptHost) c.host).appendLogFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle(), data); } return success?LazyValue.TRUE:LazyValue.FALSE; }); @@ -874,7 +878,7 @@ public class Auxiliary { if (lv.size()>0) { c.host.issueDeprecation("load_app_data(...) with arguments"); - file = recognizeResource(lv.get(0).evalValue(c)); + file = recognizeResource(lv.get(0).evalValue(c), false); if (lv.size() > 1) { shared = lv.get(1).evalValue(c).getBoolean(); @@ -897,7 +901,7 @@ public class Auxiliary { if (lv.size()>1) { c.host.issueDeprecation("store_app_data(...) with more than one argument"); - file = recognizeResource(lv.get(1).evalValue(c)); + file = recognizeResource(lv.get(1).evalValue(c), false); if (lv.size() > 2) { shared = lv.get(2).evalValue(c).getBoolean(); @@ -961,6 +965,7 @@ public class Auxiliary { { if (lv.size() == 0) throw new InternalExpressionException("'signal' requires at least one argument"); + CarpetContext cc = (CarpetContext)c; CarpetScriptServer server = ((CarpetScriptHost)c.host).getScriptServer(); String eventName = lv.get(0).evalValue(c).getString(); // no such event yet @@ -975,10 +980,10 @@ public class Auxiliary { args = FunctionValue.resolveArgs(lv.subList(2, lv.size()), c, t); } } - int counts = ((CarpetScriptHost)c.host).getScriptServer().events.signalEvent(eventName, (CarpetScriptHost) c.host, player, args); + int counts = ((CarpetScriptHost)c.host).getScriptServer().events.signalEvent(eventName, cc, player, args); if (counts < 0) return LazyValue.NULL; Value ret = new NumericValue(counts); - return (cc, tt) -> ret; + return (c_, t_) -> ret; }); } diff --git a/src/main/java/carpet/script/api/Entities.java b/src/main/java/carpet/script/api/Entities.java index 7fac7ef1..ccf35938 100644 --- a/src/main/java/carpet/script/api/Entities.java +++ b/src/main/java/carpet/script/api/Entities.java @@ -23,6 +23,7 @@ import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.nbt.CompoundTag; import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ServerWorld; import net.minecraft.util.Identifier; @@ -34,6 +35,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -183,8 +185,9 @@ public class Entities { expression.addLazyFunction("entity_list", 1, (c, t, lv) -> { String who = lv.get(0).evalValue(c).getString(); - EntityValue.EntityClassDescriptor eDesc = EntityValue.getEntityDescriptor(who); - List entityList = ((CarpetContext)c).s.getWorld().getEntitiesByType(eDesc.directType, eDesc.filteringPredicate); + ServerCommandSource source = ((CarpetContext)c).s; + EntityValue.EntityClassDescriptor eDesc = EntityValue.getEntityDescriptor(who, source.getMinecraftServer()); + List entityList = source.getWorld().getEntitiesByType(eDesc.directType, eDesc.filteringPredicate); Value retval = ListValue.wrap(entityList.stream().map(EntityValue::new).collect(Collectors.toList())); return (_c, _t ) -> retval; }); @@ -194,16 +197,26 @@ public class Entities { if (lv.size()<3) throw new InternalExpressionException("'entity_area' requires entity type, center and range arguments"); String who = lv.get(0).evalValue(c).getString(); CarpetContext cc = (CarpetContext)c; - Vector3Argument centerLocator = Vector3Argument.findIn(cc, lv, 1); - Vec3d center = centerLocator.vec; - if (centerLocator.fromBlock) center.add(0.5, 0.5, 0.5); + Vector3Argument centerLocator = Vector3Argument.findIn(cc, lv, 1, false, true); + + Box centerBox; + if (centerLocator.entity != null) + { + centerBox = centerLocator.entity.getBoundingBox(); + } + else + { + Vec3d center = centerLocator.vec; + if (centerLocator.fromBlock) center.add(0.5, 0.5, 0.5); + centerBox = new Box(center, center); + } Vector3Argument rangeLocator = Vector3Argument.findIn(cc, lv, centerLocator.offset); if (rangeLocator.fromBlock) throw new InternalExpressionException("Range of 'entity_area' cannot come from a block argument"); Vec3d range = rangeLocator.vec; - Box area = new Box(center, center).expand(range.x, range.y, range.z); - EntityValue.EntityClassDescriptor eDesc = EntityValue.getEntityDescriptor(who); - List entityList = ((CarpetContext)c).s.getWorld().getEntitiesByType(eDesc.directType, area,eDesc.filteringPredicate); + Box area = centerBox.expand(range.x, range.y, range.z); + EntityValue.EntityClassDescriptor eDesc = EntityValue.getEntityDescriptor(who, cc.s.getMinecraftServer()); + List entityList = cc.s.getWorld().getEntitiesByType(eDesc.directType, area,eDesc.filteringPredicate); Value retval = ListValue.wrap(entityList.stream().map(EntityValue::new).collect(Collectors.toList())); return (_c, _t ) -> retval; }); @@ -228,7 +241,9 @@ public class Entities { Value v = lv.get(0).evalValue(c); if (!(v instanceof EntityValue)) throw new InternalExpressionException("First argument to query should be an entity"); - String what = lv.get(1).evalValue(c).getString(); + String what = lv.get(1).evalValue(c).getString().toLowerCase(Locale.ROOT); + if (what.equals("tags")) + c.host.issueDeprecation("'tags' for entity querying"); Value retval; if (lv.size()==2) retval = ((EntityValue) v).get(what, null); @@ -259,11 +274,13 @@ public class Entities { return (cc, tt) -> v; }); - expression.addFunction("entity_types", (lv) -> + expression.addLazyFunction("entity_types", -1, (c, t, lv) -> { + if (lv.size() > 1) throw new InternalExpressionException("'entity_types' requires one or no arguments"); - String desc = (lv.size() == 1)?lv.get(0).getString():"*"; - return EntityValue.getEntityDescriptor(desc).listValue; + String desc = (lv.size() == 1)?lv.get(0).evalValue(c).getString():"*"; + Value ret = EntityValue.getEntityDescriptor(desc, ((CarpetContext) c).s.getMinecraftServer()).listValue; + return (_c, _t) -> ret; }); expression.addLazyFunction("entity_load_handler", -1, (c, t, lv) -> @@ -275,7 +292,7 @@ public class Entities { ? ((ListValue) entityValue).getItems().stream().map(Value::getString).collect(Collectors.toList()) : Collections.singletonList(entityValue.getString()); Set> types = new HashSet<>(); - descriptors.forEach(s -> types.addAll(EntityValue.getEntityDescriptor(s).typeList)); + descriptors.forEach(s -> types.addAll(EntityValue.getEntityDescriptor(s, ((CarpetContext) c).s.getMinecraftServer()).typeList)); FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 1, true, false); CarpetEventServer events = ((CarpetScriptHost)c.host).getScriptServer().events; if (funArg.function == null) diff --git a/src/main/java/carpet/script/api/Inventories.java b/src/main/java/carpet/script/api/Inventories.java index b5f6bfb4..fb4983ae 100644 --- a/src/main/java/carpet/script/api/Inventories.java +++ b/src/main/java/carpet/script/api/Inventories.java @@ -12,6 +12,7 @@ import carpet.script.value.NullValue; import carpet.script.value.NumericValue; import carpet.script.value.StringValue; import carpet.script.value.Value; +import carpet.script.value.ValueConversions; import com.google.common.collect.Sets; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.command.argument.ItemStackArgument; @@ -32,6 +33,7 @@ import net.minecraft.recipe.ShapedRecipe; import net.minecraft.recipe.ShapelessRecipe; import net.minecraft.recipe.SpecialCraftingRecipe; import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.tag.TagManager; import net.minecraft.util.Identifier; import net.minecraft.util.InvalidIdentifierException; import net.minecraft.util.math.Vec3d; @@ -40,6 +42,7 @@ import net.minecraft.util.registry.Registry; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; public class Inventories { public static void apply(Expression expression) @@ -60,6 +63,43 @@ public class Inventories { return (_c, _t) -> res; }); + expression.addLazyFunction("item_list", -1, (c, t, lv) -> + { + if (lv.size() == 0) + { + Value res = ListValue.wrap(Registry.ITEM.getIds().stream().map(ValueConversions::of).collect(Collectors.toList())); + return (cc, tt) -> res; + } + CarpetContext cc = (CarpetContext)c; + TagManager tagManager = cc.s.getMinecraftServer().getTagManager(); + String tag = lv.get(0).evalValue(c).getString(); + net.minecraft.tag.Tag itemTag = tagManager.getItems().getTag(new Identifier(tag)); + if (itemTag == null) return LazyValue.NULL; + Value ret = ListValue.wrap(itemTag.values().stream().map(b -> ValueConversions.of(Registry.ITEM.getId(b))).collect(Collectors.toList())); + return (_c, _t) -> ret; + }); + + expression.addLazyFunction("item_tags", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext)c; + TagManager tagManager = cc.s.getMinecraftServer().getTagManager(); + if (lv.size() == 0) + { + Value ret = ListValue.wrap(tagManager.getItems().getTagIds().stream().map(ValueConversions::of).collect(Collectors.toList())); + return (_c, _t) -> ret; + } + Item item = NBTSerializableValue.parseItem(lv.get(0).evalValue(c).getString()).getItem(); + if (lv.size() == 1) + { + Value ret = ListValue.wrap(tagManager.getItems().getTagsFor(item).stream().map(ValueConversions::of).collect(Collectors.toList())); + return (_c, _t) -> ret; + } + String tag = lv.get(1).evalValue(c).getString(); + net.minecraft.tag.Tag itemTag = tagManager.getItems().getTag(new Identifier(tag)); + if (itemTag == null) return LazyValue.NULL; + return item.isIn(itemTag)?LazyValue.TRUE:LazyValue.FALSE; + }); + expression.addLazyFunction("recipe_data", -1, (c, t, lv) -> { CarpetContext cc = (CarpetContext)c; diff --git a/src/main/java/carpet/script/api/Scoreboards.java b/src/main/java/carpet/script/api/Scoreboards.java index 89a27a1d..44968d4e 100644 --- a/src/main/java/carpet/script/api/Scoreboards.java +++ b/src/main/java/carpet/script/api/Scoreboards.java @@ -15,6 +15,9 @@ import carpet.script.value.FormattedTextValue; import carpet.script.value.Value; import com.google.common.collect.Lists; import net.minecraft.entity.Entity; +import net.minecraft.entity.boss.BossBar; +import net.minecraft.entity.boss.BossBarManager; +import net.minecraft.entity.boss.CommandBossBar; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.scoreboard.Scoreboard; import net.minecraft.scoreboard.ServerScoreboard; @@ -23,9 +26,12 @@ import net.minecraft.scoreboard.ScoreboardObjective; import net.minecraft.scoreboard.ScoreboardPlayerScore; import net.minecraft.scoreboard.Team; import net.minecraft.scoreboard.AbstractTeam; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.LiteralText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.InvalidIdentifierException; import java.util.stream.Collectors; @@ -404,6 +410,131 @@ public class Scoreboards { } return LazyValue.TRUE; }); + + expression.addLazyFunction("bossbar", -1, (c, t, lv) -> + { + BossBarManager bossBarManager = ((CarpetContext)c).s.getMinecraftServer().getBossBarManager(); + if(lv.size() > 3) throw new InternalExpressionException("'bossbar' accepts max three arguments"); + + if(lv.size() == 0) { + Value ret = ListValue.wrap(bossBarManager.getAll().stream().map(CommandBossBar::getId).map(Identifier::toString).map(StringValue::of).collect(Collectors.toList())); + return (_c, _t) -> ret; + } + + String id = lv.get(0).evalValue(c).getString(); + Identifier identifier; + try { + identifier = new Identifier(id); + } catch (InvalidIdentifierException invalidIdentifierException) { + return LazyValue.NULL; + } + + if(lv.size() == 1) { + if(bossBarManager.get(identifier) != null) return LazyValue.FALSE; + Value ret = StringValue.of(bossBarManager.add(identifier,new LiteralText(id)).getId().toString()); + return (_c, _t) -> ret; + } + + String property = lv.get(1).evalValue(c).getString(); + + CommandBossBar bossBar = bossBarManager.get(identifier); + if(bossBar == null) return LazyValue.NULL; + + Value propertyValue = (lv.size() == 3)?lv.get(2).evalValue(c):null; + + switch (property) { + case "color": + if(propertyValue == null) { + BossBar.Color color = (bossBar).getColor(); + if(color == null) return LazyValue.NULL; + return (_c, _t) -> StringValue.of(color.getName()); + } + + BossBar.Color color = BossBar.Color.byName(propertyValue.getString()); + if(color == null) return LazyValue.NULL; + bossBar.setColor(BossBar.Color.byName(propertyValue.getString())); + return LazyValue.TRUE; + case "max": + if(propertyValue == null) return (_c, _t) -> NumericValue.of(bossBar.getMaxValue()); + + if(!(propertyValue instanceof NumericValue)) throw new InternalExpressionException("'bossbar' requires a number as the value for the property " + property); + bossBar.setMaxValue(((NumericValue) propertyValue).getInt()); + return LazyValue.TRUE; + case "name": + if(propertyValue == null) return (_c, _t) -> new FormattedTextValue(bossBar.getName()); + + if(propertyValue instanceof FormattedTextValue) { + bossBar.setName(((FormattedTextValue) propertyValue).getText()); + } else { + bossBar.setName(new LiteralText(propertyValue.getString())); + } + return LazyValue.TRUE; + case "add_player": + if(propertyValue == null) throw new InternalExpressionException("Bossbar property " + property + " can't be queried, add a third parameter"); + + if(propertyValue instanceof ListValue) { + ((ListValue) propertyValue).getItems().forEach((v)->{ + ServerPlayerEntity player = EntityValue.getPlayerByValue(((CarpetContext)c).s.getMinecraftServer(),propertyValue); + if(player != null) bossBar.addPlayer(player); + }); + return LazyValue.TRUE; + } + + ServerPlayerEntity player = EntityValue.getPlayerByValue(((CarpetContext) c).s.getMinecraftServer(), propertyValue); + if(player != null) { + bossBar.addPlayer(player); + return LazyValue.TRUE; + } + return LazyValue.FALSE; + case "players": + if(propertyValue == null) { + return (_c, _t) -> ListValue.wrap(bossBar.getPlayers().stream().map(EntityValue::new).collect(Collectors.toList())); + } + + if(propertyValue instanceof ListValue) { + bossBar.clearPlayers(); + ((ListValue) propertyValue).getItems().forEach((v) -> { + ServerPlayerEntity p = EntityValue.getPlayerByValue(((CarpetContext) c).s.getMinecraftServer(), v); + if (p != null) bossBar.addPlayer(p); + }); + return LazyValue.TRUE; + } + + + ServerPlayerEntity p = EntityValue.getPlayerByValue(((CarpetContext) c).s.getMinecraftServer(), propertyValue); + bossBar.clearPlayers(); + if (p != null) { + bossBar.addPlayer(p); + return LazyValue.TRUE; + } + return LazyValue.FALSE; + case "style": + if(propertyValue == null) { + return (_c, _t) -> StringValue.of(bossBar.getOverlay().getName()); + } + + BossBar.Style style = BossBar.Style.byName(propertyValue.getString()); + if(style == null) throw new InternalExpressionException("'" + propertyValue.getString() + "' is not a valid value for property " + property); + bossBar.setOverlay(style); + return LazyValue.TRUE; + case "value": + if(propertyValue == null) return (_c, _t) -> NumericValue.of(bossBar.getValue()); + + if(!(propertyValue instanceof NumericValue)) throw new InternalExpressionException("'bossbar' requires a number as the value for the property " + property); + bossBar.setValue(((NumericValue) propertyValue).getInt()); + return LazyValue.TRUE; + case "visible": + if(propertyValue == null) return (_c, _t) -> new NumericValue(bossBar.isVisible()); + + bossBar.setVisible(propertyValue.getBoolean()); + return LazyValue.TRUE; + case "remove": + bossBarManager.remove(bossBar); + return LazyValue.TRUE; + default: + throw new InternalExpressionException("Unknown bossbar property " + property); + } + }); } } diff --git a/src/main/java/carpet/script/api/WorldAccess.java b/src/main/java/carpet/script/api/WorldAccess.java index fa7a6361..d15da361 100644 --- a/src/main/java/carpet/script/api/WorldAccess.java +++ b/src/main/java/carpet/script/api/WorldAccess.java @@ -6,6 +6,7 @@ import carpet.fakes.BiomeArrayInterface; import carpet.fakes.ChunkGeneratorInterface; import carpet.fakes.ChunkTicketManagerInterface; import carpet.fakes.ServerChunkManagerInterface; +import carpet.fakes.ServerWorldInterface; import carpet.fakes.SpawnHelperInnerInterface; import carpet.fakes.ThreadedAnvilChunkStorageInterface; import carpet.helpers.FeatureGenerator; @@ -58,6 +59,7 @@ import net.minecraft.item.SwordItem; import net.minecraft.item.TridentItem; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; +import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.world.ChunkTicket; import net.minecraft.server.world.ChunkTicketType; @@ -89,6 +91,7 @@ import net.minecraft.world.gen.ChunkRandom; import net.minecraft.world.gen.chunk.ChunkGenerator; import net.minecraft.world.gen.feature.ConfiguredStructureFeature; import net.minecraft.world.gen.feature.StructureFeature; +import net.minecraft.world.level.ServerWorldProperties; import net.minecraft.world.poi.PointOfInterest; import net.minecraft.world.poi.PointOfInterestStorage; import net.minecraft.world.poi.PointOfInterestType; @@ -413,6 +416,67 @@ public class WorldAccess { }); + expression.addLazyFunction("weather",-1,(c, t, lv) -> { + ServerWorld world = ((CarpetContext) c).s.getWorld(); + + if(lv.size()==0)//cos it can thunder when raining or when clear. + { + Value ret = new StringValue(world.isThundering() ? "thunder" : (world.isRaining() ? "rain" : "clear")); + return (_c, _t) -> ret; + } + + Value weather = lv.get(0).evalValue(c); + ServerWorldProperties worldProperties = ((ServerWorldInterface) world).getWorldPropertiesCM(); + if(lv.size()==1) + { + int ticks; + switch (weather.getString().toLowerCase(Locale.ROOT)) { + case "clear": + ticks = worldProperties.getClearWeatherTime(); + break; + case "rain": + ticks = world.isRaining()? worldProperties.getRainTime():0;//cos if not it gives 1 for some reason + break; + case "thunder": + ticks = world.isThundering()? worldProperties.getThunderTime():0;//same dealio here + break; + default: + throw new InternalExpressionException("Weather can only be 'clear', 'rain' or 'thunder'"); + } + Value ret = new NumericValue(ticks); + return (_c,_t) -> ret; + } + if(lv.size()==2) + { + int ticks = NumericValue.asNumber(lv.get(1).evalValue(c), "tick_time in 'weather'").getInt(); + switch (weather.getString().toLowerCase(Locale.ROOT)) + { + case "clear": + world.setWeather(ticks,0,false,false); + break; + + case "rain": + world.setWeather(0,ticks,true,false); + break; + + case "thunder": + world.setWeather( + 0, + ticks,//this is used to set thunder time, idk why... + true, + true + ); + break; + + default: + throw new InternalExpressionException("Weather can only be 'clear', 'rain' or 'thunder'"); + } + Value ret = NumericValue.of(ticks); + return (_c, _t) -> ret; + } + throw new InternalExpressionException("'weather' requires 0, 1 or 2 arguments"); + }); + expression.addLazyFunction("pos", 1, (c, t, lv) -> { Value arg = lv.get(0).evalValue(c); @@ -713,6 +777,21 @@ public class WorldAccess { } args = ((ListValue) args.get(0)).getItems(); } + else if (args.get(0) instanceof MapValue) + { + if (args.size() == 2) + { + Value dataValue = NBTSerializableValue.fromValue( args.get(1)); + if (dataValue instanceof NBTSerializableValue) + { + data = ((NBTSerializableValue) dataValue).getCompoundTag(); + } + } + Map state = ((MapValue) args.get(0)).getMap(); + List mapargs = new ArrayList<>(); + state.forEach( (k, v) -> {mapargs.add(k); mapargs.add(v);}); + args = mapargs; + } else { if ((args.size() & 1) == 1) @@ -748,7 +827,7 @@ public class WorldAccess { { Clearable.clear(world.getBlockEntity(targetPos)); boolean success = world.setBlockState(targetPos, finalSourceBlockState, 2); - if (success && finalData != null) + if (finalData != null) { BlockEntity be = world.getBlockEntity(targetPos); if (be != null) @@ -759,6 +838,7 @@ public class WorldAccess { destTag.putInt("z", targetPos.getZ()); be.fromTag(finalSourceBlockState, destTag); be.markDirty(); + success = true; } } result[0] = success; diff --git a/src/main/java/carpet/script/argument/Vector3Argument.java b/src/main/java/carpet/script/argument/Vector3Argument.java index 6c408089..003ffc4c 100644 --- a/src/main/java/carpet/script/argument/Vector3Argument.java +++ b/src/main/java/carpet/script/argument/Vector3Argument.java @@ -4,9 +4,11 @@ import carpet.script.CarpetContext; import carpet.script.LazyValue; import carpet.script.exception.InternalExpressionException; import carpet.script.value.BlockValue; +import carpet.script.value.EntityValue; import carpet.script.value.ListValue; import carpet.script.value.NumericValue; import carpet.script.value.Value; +import net.minecraft.entity.Entity; import net.minecraft.util.math.Vec3d; import java.util.List; @@ -16,7 +18,8 @@ public class Vector3Argument extends Argument public Vec3d vec; public final double yaw; public final double pitch; - public boolean fromBlock; + public boolean fromBlock = false; + public Entity entity = null; private Vector3Argument(Vec3d v, int o) { super(o); @@ -38,13 +41,19 @@ public class Vector3Argument extends Argument return this; } + private Vector3Argument withEntity(Entity e) + { + entity = e; + return this; + } + public static Vector3Argument findIn(CarpetContext c, List params, int offset) { - return findIn(c,params, offset, false); + return findIn(c,params, offset, false, false); } - public static Vector3Argument findIn(CarpetContext c, List params, int offset, boolean optionalDirection) + public static Vector3Argument findIn(CarpetContext c, List params, int offset, boolean optionalDirection, boolean optionalEntity) { try { @@ -54,6 +63,11 @@ public class Vector3Argument extends Argument // pos + 0.5v return (new Vector3Argument(Vec3d.ofCenter(((BlockValue) v1).getPos()), 1+offset)).fromBlock(); } + if (optionalEntity && v1 instanceof EntityValue) + { + Entity e = ((EntityValue) v1).getEntity(); + return new Vector3Argument(e.getPos(), 1+offset).withEntity(e); + } if (v1 instanceof ListValue) { List args = ((ListValue) v1).getItems(); diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java index 031c862c..b710b376 100644 --- a/src/main/java/carpet/script/bundled/FileModule.java +++ b/src/main/java/carpet/script/bundled/FileModule.java @@ -1,5 +1,7 @@ package carpet.script.bundled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.PositionTracker; import net.minecraft.nbt.Tag; import net.minecraft.nbt.TagReaders; @@ -16,23 +18,25 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.stream.Stream; public class FileModule extends Module { private String name; private String code; private boolean library; - public FileModule(File sourceFile) + public FileModule(Path sourcePath) { - library = sourceFile.getName().endsWith(".scl"); + library = sourcePath.getFileName().toString().endsWith(".scl"); try { - name = sourceFile.getName().replaceFirst("\\.scl?","").toLowerCase(Locale.ROOT); - code = new String(Files.readAllBytes(sourceFile.toPath()), StandardCharsets.UTF_8); + name = sourcePath.getFileName().toString().replaceFirst("\\.scl?","").toLowerCase(Locale.ROOT); + code = new String(Files.readAllBytes(sourcePath), StandardCharsets.UTF_8); } catch ( IOException e) { @@ -59,25 +63,40 @@ public class FileModule extends Module } //copied private method from net.minecraft.nbt.NbtIo.read() + // to read non-compound tags - these won't be compressed public static Tag read(File file) { - try (DataInputStream dataInput_1 = new DataInputStream(new FileInputStream(file))) + try { - byte byte_1 = dataInput_1.readByte(); - if (byte_1 == 0) + return NbtIo.readCompressed(file); + } + catch (IOException e) + { + try { + return NbtIo.read(file); + } + catch (IOException ioException) + { + try (DataInputStream dataInput_1 = new DataInputStream(new FileInputStream(file))) + { + byte byte_1 = dataInput_1.readByte(); + if (byte_1 == 0) + { + return null; + } + else + { + dataInput_1.readUTF(); + return TagReaders.of(byte_1).read(dataInput_1, 0, PositionTracker.DEFAULT); + } + } + catch (IOException ignored) + { + } return null; } - else - { - dataInput_1.readUTF(); - return TagReaders.of(byte_1).read(dataInput_1, 0, PositionTracker.DEFAULT); - } } - catch (IOException ignored) - { - } - return null; } //copied private method from net.minecraft.nbt.NbtIo.write() and client method safe_write @@ -86,18 +105,32 @@ public class FileModule extends Module File file_2 = new File(file.getAbsolutePath() + "_tmp"); if (file_2.exists()) file_2.delete(); - try(DataOutputStream dataOutputStream_1 = new DataOutputStream(new FileOutputStream(file_2))) + if (tag_1 instanceof CompoundTag) { - dataOutputStream_1.writeByte(tag_1.getType()); - if (tag_1.getType() != 0) + try { - dataOutputStream_1.writeUTF(""); - tag_1.write(dataOutputStream_1); + NbtIo.writeCompressed((CompoundTag) tag_1, file_2); + } + catch (IOException e) + { + return false; } } - catch (IOException e) + else { - return false; + try (DataOutputStream dataOutputStream_1 = new DataOutputStream(new FileOutputStream(file_2))) + { + dataOutputStream_1.writeByte(tag_1.getType()); + if (tag_1.getType() != 0) + { + dataOutputStream_1.writeUTF(""); + tag_1.write(dataOutputStream_1); + } + } + catch (IOException e) + { + return false; + } } if (file.exists()) file.delete(); if (!file.exists()) file_2.renameTo(file); @@ -143,4 +176,18 @@ public class FileModule extends Module return null; } } + + public static Stream listFiles(Path dir, String ext) + { + boolean folderListing = ext.equals("folder"); + try + { + return Files.list(dir). + filter(path -> folderListing?Files.isDirectory(path):path.toString().endsWith(ext)); + } + catch (IOException ignored) + { + return null; + } + } } diff --git a/src/main/java/carpet/script/bundled/Module.java b/src/main/java/carpet/script/bundled/Module.java index 1916e0ba..3bf5f8ce 100644 --- a/src/main/java/carpet/script/bundled/Module.java +++ b/src/main/java/carpet/script/bundled/Module.java @@ -3,10 +3,12 @@ package carpet.script.bundled; import carpet.CarpetServer; import net.minecraft.nbt.Tag; import net.minecraft.util.WorldSavePath; +import org.apache.commons.io.FilenameUtils; -import java.io.File; import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; +import java.util.stream.Stream; public abstract class Module { @@ -19,61 +21,73 @@ public abstract class Module { if (isShared) { - return "shared/"+file; + return file.isEmpty()?"shared":"shared/"+file; } - else if (module != null && module.getName() != null) // appdata + if (module != null && module.getName() != null) // appdata { - return module.getName()+".data"+(file==null?"":"/"+file); - } - else - { - throw new RuntimeException("Invalid file descriptor: "+file); + return module.getName()+".data"+(file==null || file.isEmpty()?"":"/"+file); } + throw new RuntimeException("Invalid file descriptor: "+file); } public static Tag getData(Module module, String file, boolean isShared) { - File dataFile = resolveResource(module, file, "nbt", isShared); + Path dataFile = resolveResource(module, file, "nbt", isShared); if (dataFile == null) return null; - if (!Files.exists(dataFile.toPath()) || !(dataFile.isFile())) return null; - synchronized (writeIOSync) { return FileModule.read(dataFile); } + if (!Files.exists(dataFile) || !(dataFile.toFile().isFile())) return null; + synchronized (writeIOSync) { return FileModule.read(dataFile.toFile()); } } public static boolean saveData(Module module, String file, Tag globalState, boolean isShared) { - File dataFile = resolveResource(module, file, "nbt", isShared); + Path dataFile = resolveResource(module, file, "nbt", isShared); if (dataFile == null) return false; - if (!Files.exists(dataFile.toPath().getParent()) && !dataFile.getParentFile().mkdirs()) return false; - synchronized (writeIOSync) { return FileModule.write(globalState, dataFile); } + if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false; + synchronized (writeIOSync) { return FileModule.write(globalState, dataFile.toFile()); } } public static boolean appendToTextFile(Module module, String resourceName, String type, boolean isShared, List message) { - File dataFile = resolveResource(module, resourceName, "txt", isShared); + Path dataFile = resolveResource(module, resourceName, "txt", isShared); if (dataFile == null) return false; - if (!Files.exists(dataFile.toPath().getParent()) && !dataFile.getParentFile().mkdirs()) return false; - synchronized (writeIOSync) { return FileModule.appendText(dataFile, type.equals("text"), message); } + if (!Files.exists(dataFile.getParent()) && !dataFile.toFile().getParentFile().mkdirs()) return false; + synchronized (writeIOSync) { return FileModule.appendText(dataFile.toFile(), type.equals("text"), message); } } public static boolean dropExistingFile(Module module, String resourceName, String ext, boolean isShared) { - File dataFile = resolveResource(module, resourceName, ext, isShared); + Path dataFile = resolveResource(module, resourceName, ext, isShared); if (dataFile == null) return false; - synchronized (writeIOSync) { return dataFile.delete(); } + synchronized (writeIOSync) { return dataFile.toFile().delete(); } } public static List listFile(Module module, String resourceName, String ext, boolean isShared) { - File dataFile = resolveResource(module, resourceName, ext, isShared); + Path dataFile = resolveResource(module, resourceName, ext, isShared); if (dataFile == null) return null; - if (!dataFile.exists()) return null; - synchronized (writeIOSync) { return FileModule.listFileContent(dataFile); } + if (!dataFile.toFile().exists()) return null; + synchronized (writeIOSync) { return FileModule.listFileContent(dataFile.toFile()); } } - private static File resolveResource(Module module, String resourceName, String ext, boolean isShared) + public static Stream listFolder(Module module, String resourceName, String ext, boolean isShared) + { + Path dir = resolveResource(module, resourceName, null, isShared); + if (dir == null) return null; + if (!Files.exists(dir)) return null; + Stream result; + synchronized (writeIOSync) { result = FileModule.listFiles(dir, ext); } + if (result == null) return null; + Path rootPath = resolveResource(module, "", null, isShared); + Stream strings = result.map(p -> rootPath.relativize(p).toString().replaceAll("[\\\\/]","/")); + if (!ext.equals("folder")) + return strings.map(FilenameUtils::removeExtension); + return strings; + } + + private static Path resolveResource(Module module, String resourceName, String ext, boolean isShared) { if (!isShared && (module == null || module.getName() == null)) return null; - return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+getDescriptor(module, resourceName, isShared)+"."+ext).toFile(); + return CarpetServer.minecraft_server.getSavePath(WorldSavePath.ROOT).resolve("scripts/"+getDescriptor(module, resourceName, isShared)+(ext==null?"":"."+ext)); } @Override diff --git a/src/main/java/carpet/script/command/CommandArgument.java b/src/main/java/carpet/script/command/CommandArgument.java index 1ddede7f..d2ea2f7d 100644 --- a/src/main/java/carpet/script/command/CommandArgument.java +++ b/src/main/java/carpet/script/command/CommandArgument.java @@ -63,6 +63,7 @@ import net.minecraft.command.argument.RotationArgumentType; import net.minecraft.command.argument.ScoreHolderArgumentType; import net.minecraft.command.argument.ScoreboardSlotArgumentType; import net.minecraft.command.argument.SwizzleArgumentType; +import net.minecraft.command.argument.TeamArgumentType; import net.minecraft.command.argument.TimeArgumentType; import net.minecraft.command.argument.UuidArgumentType; import net.minecraft.command.argument.Vec2ArgumentType; @@ -130,7 +131,6 @@ public abstract class CommandArgument new VanillaUnconfigurableArgument( "blockpredicate", BlockPredicateArgumentType::blockPredicate, (c, p) -> ValueConversions.ofBlockPredicate(c.getSource().getMinecraftServer().getTagManager(), BlockPredicateArgumentType.getBlockPredicate(c, p)), false ), - // block_predicate todo - not sure about the returned format. Needs to match block tags used in the API (future) new VanillaUnconfigurableArgument("teamcolor", ColorArgumentType::color, (c, p) -> { Formatting format = ColorArgumentType.getColor(c, p); @@ -165,7 +165,6 @@ public abstract class CommandArgument (c, p) -> ValueConversions.of(Registry.ENCHANTMENT.getId(ItemEnchantmentArgumentType.getEnchantment(c, p))), false ), // item_predicate ?? //same as item but accepts tags, not sure right now - // slot // item_slot new SlotArgument(), new VanillaUnconfigurableArgument("item", ItemStackArgumentType::itemStack, (c, p) -> ValueConversions.of(ItemStackArgumentType.getItemStackArgument(c, p).createStack(1, false)), false @@ -241,7 +240,9 @@ public abstract class CommandArgument new VanillaUnconfigurableArgument("swizzle", SwizzleArgumentType::swizzle, (c, p) -> StringValue.of(SwizzleArgumentType.getSwizzle(c, p).stream().map(Direction.Axis::asString).collect(Collectors.joining())), true ), - // team + new VanillaUnconfigurableArgument("team", TeamArgumentType::team, + (c, p) -> StringValue.of(TeamArgumentType.getTeam(c, p).getName()), false + ), new VanillaUnconfigurableArgument("time", TimeArgumentType::time, (c, p) -> new NumericValue(IntegerArgumentType.getInteger(c, p)), false ), @@ -437,7 +438,7 @@ public abstract class CommandArgument if (config.containsKey("options")) { Value optionsValue = config.get("options"); - if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom sting type requires options passed as a list"); + if (!(optionsValue instanceof ListValue)) throw new InternalExpressionException("Custom string type requires options passed as a list"); validOptions = ((ListValue) optionsValue).getItems().stream() .map(v -> caseSensitive?v.getString():(v.getString().toLowerCase(Locale.ROOT))) .collect(Collectors.toSet()); diff --git a/src/main/java/carpet/script/language/DataStructures.java b/src/main/java/carpet/script/language/DataStructures.java index c61f5d42..10c21c94 100644 --- a/src/main/java/carpet/script/language/DataStructures.java +++ b/src/main/java/carpet/script/language/DataStructures.java @@ -135,23 +135,25 @@ public class DataStructures { expression.addFunction("range", (lv) -> { - long from = 0; - long to = 0; - long step = 1; + NumericValue from = Value.ZERO; + NumericValue to; + NumericValue step = Value.ONE; int argsize = lv.size(); if (argsize == 0 || argsize > 3) throw new InternalExpressionException("'range' accepts from 1 to 3 arguments, not "+argsize); - to = NumericValue.asNumber(lv.get(0)).getLong(); + to = NumericValue.asNumber(lv.get(0)); if (lv.size() > 1) { from = to; - to = NumericValue.asNumber(lv.get(1)).getLong(); + to = NumericValue.asNumber(lv.get(1)); if (lv.size() > 2) { - step = NumericValue.asNumber(lv.get(2)).getLong(); + step = NumericValue.asNumber(lv.get(2)); } } - return LazyListValue.range(from, to, step); + return (from.isInteger() && to.isInteger() && step.isInteger()) + ? LazyListValue.rangeLong(from.getLong(), to.getLong(), step.getLong()) + : LazyListValue.rangeDouble(from.getDouble(), to.getDouble(), step.getDouble()); }); expression.addLazyFunction("m", -1, (c, t, llv) -> diff --git a/src/main/java/carpet/script/utils/FixedCommandSource.java b/src/main/java/carpet/script/utils/FixedCommandSource.java new file mode 100644 index 00000000..c88e9874 --- /dev/null +++ b/src/main/java/carpet/script/utils/FixedCommandSource.java @@ -0,0 +1,206 @@ +package carpet.script.utils; + +import carpet.CarpetSettings; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.Value; +import com.mojang.brigadier.ResultConsumer; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.command.argument.EntityAnchorArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.CommandOutput; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.math.Vec2f; +import net.minecraft.util.math.Vec3d; + +import java.util.List; +import java.util.function.BinaryOperator; + +public class FixedCommandSource extends ServerCommandSource +{ + private Vec3d position; + private ServerWorld world; + private final String simpleName; + private final Text name; + private final MinecraftServer server; + private Entity entity; + private ResultConsumer resultConsumer; + private EntityAnchorArgumentType.EntityAnchor entityAnchor; + private Vec2f rotation; + private final Value[] error; + private final List chatOutput; + + public FixedCommandSource(ServerCommandSource original, Vec3d pos, Value[] error, List chatOutput) + { + super(CommandOutput.DUMMY, pos, Vec2f.ZERO, original.getWorld(), CarpetSettings.runPermissionLevel, + original.getName(), original.getDisplayName(), original.getMinecraftServer(), original.getEntity(), true, + (ctx, succ, res) -> { }, EntityAnchorArgumentType.EntityAnchor.FEET); + position = pos; + world = original.getWorld(); + simpleName = original.getName(); + name = original.getDisplayName(); + server = original.getMinecraftServer(); + entity = original.getEntity(); + resultConsumer = (ctx, succ, res) -> { }; + entityAnchor = original.getEntityAnchor(); + rotation = Vec2f.ZERO; + this.error = error; + this.chatOutput = chatOutput; + } + + @Override + public ServerCommandSource withEntity(Entity entity) + { + this.entity = entity; + return this; + } + + @Override + public ServerCommandSource withPosition(Vec3d position) + { + this.position = position; + return this; + } + + @Override + public ServerCommandSource withRotation(Vec2f rotation) + { + this.rotation = rotation; + return this; + } + + @Override + public ServerCommandSource withConsumer(ResultConsumer consumer) + { + this.resultConsumer = consumer; + return this; + } + + @Override + public ServerCommandSource mergeConsumers(ResultConsumer consumer, BinaryOperator> binaryOperator) + { + return this.withConsumer(binaryOperator.apply(this.resultConsumer, consumer)); + } + + @Override + public ServerCommandSource withSilent() + { + return this; + } + + @Override + public ServerCommandSource withLevel(int level) + { + return this; + } + + @Override + public ServerCommandSource withMaxLevel(int level) + { + return this; + } + + @Override + public ServerCommandSource withEntityAnchor(EntityAnchorArgumentType.EntityAnchor anchor) + { + entityAnchor = anchor; + return this; + } + + @Override + public ServerCommandSource withWorld(ServerWorld world) + { + this.world = world; + return this; + } + + @Override + public Text getDisplayName() + { + return name; + } + + @Override + public String getName() + { + return simpleName; + } + + @Override + public Vec3d getPosition() + { + return position; + } + + @Override + public ServerWorld getWorld() + { + return world; + } + + @Override + public Entity getEntity() + { + return entity; + } + + @Override + public Entity getEntityOrThrow() throws CommandSyntaxException + { + if (entity == null) { + throw REQUIRES_ENTITY_EXCEPTION.create(); + } else { + return entity; + } + } + + @Override + public ServerPlayerEntity getPlayer() throws CommandSyntaxException + { + if (!(this.entity instanceof ServerPlayerEntity)) { + throw REQUIRES_PLAYER_EXCEPTION.create(); + } else { + return (ServerPlayerEntity)this.entity; + } + } + + @Override + public Vec2f getRotation() + { + return rotation; + } + + @Override + public MinecraftServer getMinecraftServer() + { + return server; + } + + @Override + public EntityAnchorArgumentType.EntityAnchor getEntityAnchor() + { + return entityAnchor; + } + + + @Override + public void onCommandComplete(CommandContext context, boolean success, int result) + { + } + + @Override + public void sendError(Text message) + { + error[0] = new FormattedTextValue(message); + } + @Override + public void sendFeedback(Text message, boolean broadcastToOps) + { + chatOutput.add(new FormattedTextValue(message)); + } + +} diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java index 2dc048db..8d1ac7c2 100644 --- a/src/main/java/carpet/script/utils/SystemInfo.java +++ b/src/main/java/carpet/script/utils/SystemInfo.java @@ -1,6 +1,7 @@ package carpet.script.utils; import carpet.CarpetServer; +import carpet.CarpetSettings; import carpet.script.CarpetContext; import carpet.script.CarpetScriptHost; import carpet.script.value.ListValue; @@ -11,7 +12,9 @@ import carpet.script.value.Value; import carpet.settings.ParsedRule; import carpet.settings.SettingsManager; import com.sun.management.OperatingSystemMXBean; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.util.WorldSavePath; +import net.minecraft.world.GameRules; import java.lang.management.ManagementFactory; import java.nio.file.Path; @@ -80,6 +83,7 @@ public class SystemInfo { } return whitelist; }); + put("server_dev_environment", c-> new NumericValue(FabricLoader.getInstance().isDevelopmentEnvironment())); put("java_max_memory", c -> new NumericValue(Runtime.getRuntime().maxMemory())); put("java_allocated_memory", c -> new NumericValue(Runtime.getRuntime().totalMemory())); @@ -115,7 +119,7 @@ public class SystemInfo { CarpetServer.extensions.forEach(e -> { SettingsManager manager = e.customSettingsManager(); if (manager == null) return; - + Collection> extensionRules = manager.getRules(); extensionRules.forEach(rule -> { carpetRules.put(new StringValue(manager.getIdentifier()+":"+rule.name), new StringValue(rule.getAsString())); @@ -123,14 +127,18 @@ public class SystemInfo { }); return carpetRules; }); - - - - - - - - + put("world_gamerules", c->{ + Map rules = new HashMap<>(); + final GameRules gameRules = c.s.getWorld().getGameRules(); + GameRules.accept(new GameRules.Visitor() { + @Override + public > void visit(GameRules.Key key, GameRules.Type type) { + rules.put(StringValue.of(key.getName()), StringValue.of(gameRules.get(key).toString())); + } + }); + return MapValue.wrap(rules); + }); + put("scarpet_version", c -> StringValue.of(CarpetSettings.carpetVersion)); }}; public static Value get(String what, CarpetContext cc) diff --git a/src/main/java/carpet/script/value/EntityValue.java b/src/main/java/carpet/script/value/EntityValue.java index 22078193..89535a7c 100644 --- a/src/main/java/carpet/script/value/EntityValue.java +++ b/src/main/java/carpet/script/value/EntityValue.java @@ -9,6 +9,7 @@ import carpet.fakes.MobEntityInterface; import carpet.fakes.HungerManagerInterface; import carpet.fakes.ServerPlayerInteractionManagerInterface; import carpet.helpers.Tracer; +import carpet.network.ServerNetworkHandler; import carpet.patches.EntityPlayerMPFake; import carpet.script.CarpetContext; import carpet.script.EntityEventsGroup; @@ -29,6 +30,7 @@ import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.network.packet.s2c.play.EntityPassengersSetS2CPacket; import net.minecraft.network.packet.s2c.play.EntityPositionS2CPacket; +import net.minecraft.network.packet.s2c.play.ExperienceBarUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket; import net.minecraft.command.EntitySelector; import net.minecraft.command.EntitySelectorReader; @@ -221,10 +223,28 @@ public class EntityValue extends Value return entity.hashCode(); } - public static EntityClassDescriptor getEntityDescriptor(String who) + public static EntityClassDescriptor getEntityDescriptor(String who, MinecraftServer server) { EntityClassDescriptor eDesc = EntityClassDescriptor.byName.get(who); - if (eDesc == null) throw new InternalExpressionException(who+" is not a valid entity descriptor"); + if (eDesc == null) + { + boolean positive = true; + if (who.startsWith("!")) + { + positive = false; + who = who.substring(1); + } + net.minecraft.tag.Tag> eTag = server.getTagManager().getEntityTypes().getTag(new Identifier(who)); + if (eTag == null) throw new InternalExpressionException(who+" is not a valid entity descriptor"); + if (positive) + { + return new EntityClassDescriptor(null, e -> eTag.contains(e.getType()) && e.isAlive(), eTag.values().stream()); + } + else + { + return new EntityClassDescriptor(null, e -> !eTag.contains(e.getType()) && e.isAlive(), Registry.ENTITY_TYPE.stream().filter(et -> !eTag.contains(et))); + } + } return eDesc; //TODO add more here like search by tags, or type //if (who.startsWith('tag:')) @@ -399,8 +419,21 @@ public class EntityValue extends Value put("passengers", (e, a) -> ListValue.wrap(e.getPassengerList().stream().map(EntityValue::new).collect(Collectors.toList()))); put("mount", (e, a) -> (e.getVehicle()!=null)?new EntityValue(e.getVehicle()):Value.NULL); put("unmountable", (e, a) -> new NumericValue(((EntityInterface)e).isPermanentVehicle())); + // deprecated put("tags", (e, a) -> ListValue.wrap(e.getScoreboardTags().stream().map(StringValue::new).collect(Collectors.toList()))); + + put("scoreboard_tags", (e, a) -> ListValue.wrap(e.getScoreboardTags().stream().map(StringValue::new).collect(Collectors.toList()))); + put("entity_tags", (e, a) -> ListValue.wrap(e.getServer().getTagManager().getEntityTypes().getTagsFor(e.getType()).stream().map(ValueConversions::of).collect(Collectors.toList()))); + // deprecated put("has_tag", (e, a) -> new NumericValue(e.getScoreboardTags().contains(a.getString()))); + + put("has_scoreboard_tag", (e, a) -> new NumericValue(e.getScoreboardTags().contains(a.getString()))); + put("has_entity_tag", (e, a) -> { + net.minecraft.tag.Tag> tag = e.getServer().getTagManager().getEntityTypes().getTag(new Identifier(a.getString())); + if (tag == null) return Value.NULL; + return new NumericValue(e.getType().isIn(tag)); + }); + put("yaw", (e, a)-> new NumericValue(e.yaw)); put("head_yaw", (e, a)-> { if (e instanceof LivingEntity) @@ -498,6 +531,31 @@ public class EntityValue extends Value return Value.NULL; }); + put("absorption",(e, a)->{ + if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).getAbsorptionAmount()); + return Value.NULL; + }); + + put("xp",(e, a)->{ + if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).totalExperience); + return Value.NULL; + }); + + put("xp_level", (e, a)->{ + if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).experienceLevel); + return Value.NULL; + }); + + put("xp_progress", (e, a)->{ + if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).experienceProgress); + return Value.NULL; + }); + + put("score", (e, a)->{ + if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).getScore()); + return Value.NULL; + }); + put("jumping", (e, a) -> { if (e instanceof LivingEntity) { @@ -579,6 +637,14 @@ public class EntityValue extends Value return Value.NULL; }); + put("client_brand", (e, a) -> { + if (e instanceof ServerPlayerEntity) + { + return StringValue.of(ServerNetworkHandler.getPlayerStatus((ServerPlayerEntity) e)); + } + return Value.NULL; + }); + put("team", (e, a) -> e.getScoreboardTeam()==null?Value.NULL:new StringValue(e.getScoreboardTeam().getName())); put("ping", (e, a) -> { @@ -1319,11 +1385,36 @@ public class EntityValue extends Value if(e instanceof PlayerEntity) ((PlayerEntity) e).getHungerManager().addExhaustion((int) NumericValue.asNumber(v).getLong()); }); - put("saturation", (e, v)-> { - if(e instanceof PlayerEntity) ((PlayerEntity) e).getHungerManager().setSaturationLevelClient((float)NumericValue.asNumber(v).getLong()); + put("absorption", (e, v) -> { + if (e instanceof PlayerEntity) ((PlayerEntity) e).setAbsorptionAmount((float) NumericValue.asNumber(v, "absorbtion").getLong()); }); - put("air", (e, v) -> e.setAir(NumericValue.asNumber(v).getInt())); + put("add_xp", (e, v) -> { + if (e instanceof PlayerEntity) ((PlayerEntity) e).addExperience(NumericValue.asNumber(v, "add_xp").getInt()); + }); + + put("xp_level", (e, v) -> { + if (e instanceof PlayerEntity) ((PlayerEntity) e).addExperienceLevels(NumericValue.asNumber(v, "xp_level").getInt()-((PlayerEntity) e).experienceLevel); + }); + + put("xp_progress", (e, v) -> { + if (e instanceof ServerPlayerEntity) + { + ServerPlayerEntity p = (ServerPlayerEntity) e; + p.experienceProgress = NumericValue.asNumber(v, "xp_progress").getFloat(); + p.networkHandler.sendPacket(new ExperienceBarUpdateS2CPacket(p.experienceProgress, p.totalExperience, p.experienceLevel)); + } + }); + + put("xp_score", (e, v) -> { + if (e instanceof PlayerEntity) ((PlayerEntity) e).setScore(NumericValue.asNumber(v, "xp_score").getInt()); + }); + + put("saturation", (e, v)-> { + if(e instanceof PlayerEntity) ((HungerManagerInterface) ((PlayerEntity) e).getHungerManager()).setSaturationCM(NumericValue.asNumber(v, "saturation").getFloat()); + }); + + put("air", (e, v) -> e.setAir(NumericValue.asNumber(v, "air").getInt())); put("breaking_progress", (e, a) -> { if (e instanceof ServerPlayerEntity) @@ -1372,7 +1463,7 @@ public class EntityValue extends Value EntityEventsGroup.Event event = EntityEventsGroup.Event.byName.get(eventName); if (event == null) throw new InternalExpressionException("Unknown entity event: " + eventName); - ((EntityInterface)entity).getEventContainer().addEvent(event, cc.host.getName(), fun, args); + ((EntityInterface)entity).getEventContainer().addEvent(event, cc.host, fun, args); } @Override diff --git a/src/main/java/carpet/script/value/FormattedTextValue.java b/src/main/java/carpet/script/value/FormattedTextValue.java index 218575ca..25e45bf0 100644 --- a/src/main/java/carpet/script/value/FormattedTextValue.java +++ b/src/main/java/carpet/script/value/FormattedTextValue.java @@ -3,6 +3,7 @@ package carpet.script.value; import net.minecraft.nbt.StringTag; import net.minecraft.nbt.Tag; import net.minecraft.text.BaseText; +import net.minecraft.text.LiteralText; import net.minecraft.text.Text; public class FormattedTextValue extends StringValue @@ -14,6 +15,33 @@ public class FormattedTextValue extends StringValue this.text = text; } + public static Value combine(Value left, Value right) { + BaseText text; + if (left instanceof FormattedTextValue) + { + text = (BaseText) ((FormattedTextValue) left).getText().shallowCopy(); + } + else + { + if (left instanceof NullValue) + return right; + text = new LiteralText(left.getString()); + } + + if (right instanceof FormattedTextValue) + { + text.append(((FormattedTextValue) right).getText().shallowCopy()); + return new FormattedTextValue(text); + } + else + { + if (right instanceof NullValue) + return left; + text.append(right.getString()); + return new FormattedTextValue(text); + } + } + @Override public String getString() { return text.getString(); @@ -48,10 +76,16 @@ public class FormattedTextValue extends StringValue return StringTag.of(Text.Serializer.toJson(text)); } + @Override + public Value add(Value o) { + return combine(this, o); + } + public String serialize() { return Text.Serializer.toJson(text); } + public static FormattedTextValue deserialize(String serialized) { return new FormattedTextValue(Text.Serializer.fromJson(serialized)); diff --git a/src/main/java/carpet/script/value/LazyListValue.java b/src/main/java/carpet/script/value/LazyListValue.java index 2edd637d..5c68a75a 100644 --- a/src/main/java/carpet/script/value/LazyListValue.java +++ b/src/main/java/carpet/script/value/LazyListValue.java @@ -11,7 +11,7 @@ import java.util.Locale; public abstract class LazyListValue extends AbstractListValue implements Iterator { - public static LazyListValue range(long from, long to, long step) + public static LazyListValue rangeDouble(double from, double to, double step) { return new LazyListValue() { @@ -23,10 +23,10 @@ public abstract class LazyListValue extends AbstractListValue implements Iterato this.limit = to; this.stepp = step; } - private long start; - private long current; - private long limit; - private long stepp; + private final double start; + private double current; + private final double limit; + private final double stepp; @Override public Value next() { @@ -50,7 +50,50 @@ public abstract class LazyListValue extends AbstractListValue implements Iterato @Override public String getString() { - return String.format(Locale.ROOT, "[%d, %d, ..., %d)",start, start+stepp, limit); + return String.format(Locale.ROOT, "[%s, %s, ..., %s)",NumericValue.of(start).getString(), NumericValue.of(start+stepp).getString(), NumericValue.of( limit).getString()); + } + }; + } + public static LazyListValue rangeLong(long from, long to, long step) + { + return new LazyListValue() + { + { + if (step == 0) + throw new InternalExpressionException("Range will never end with a zero step"); + this.start = from; + this.current = this.start; + this.limit = to; + this.stepp = step; + } + private final long start; + private long current; + private final long limit; + private final long stepp; + @Override + public Value next() + { + Value val = new NumericValue(current); + current += stepp; + return val; + } + + @Override + public void reset() + { + current = start; + } + + @Override + public boolean hasNext() + { + return stepp > 0?(current < limit):(current > limit); + } + + @Override + public String getString() + { + return String.format(Locale.ROOT, "[%s, %s, ..., %s)",NumericValue.of(start).getString(), NumericValue.of(start+stepp).getString(), NumericValue.of( limit).getString()); } }; } diff --git a/src/main/java/carpet/script/value/NBTSerializableValue.java b/src/main/java/carpet/script/value/NBTSerializableValue.java index 938acd20..877300dc 100644 --- a/src/main/java/carpet/script/value/NBTSerializableValue.java +++ b/src/main/java/carpet/script/value/NBTSerializableValue.java @@ -267,7 +267,7 @@ public class NBTSerializableValue extends Value implements ContainerValueInterfa } } - private static Map itemCache = new HashMap<>(); + private static final Map itemCache = new HashMap<>(); public static ItemStackArgument parseItem(String itemString) { diff --git a/src/main/java/carpet/script/value/NumericValue.java b/src/main/java/carpet/script/value/NumericValue.java index caec9776..00a506f4 100644 --- a/src/main/java/carpet/script/value/NumericValue.java +++ b/src/main/java/carpet/script/value/NumericValue.java @@ -15,6 +15,7 @@ import java.math.RoundingMode; import java.util.Locale; import static java.lang.Math.abs; +import static java.lang.Math.signum; public class NumericValue extends Value { @@ -57,6 +58,7 @@ public class NumericValue extends Value { if (value.isInfinite()) return "INFINITY"; if (value.isNaN()) return "NaN"; + if (abs(value) < epsilon) return (signum(value) < 0)?"-0":"0"; //zero rounding fails with big decimals // dobules have 16 point precision, 12 is plenty to display return BigDecimal.valueOf(value).round(displayRounding).stripTrailingZeros().toPlainString(); } @@ -306,4 +308,9 @@ public class NumericValue extends Value if (longValue != null) return new NumericValue(-longValue); return new NumericValue(-value); } + + public boolean isInteger() + { + return longValue!= null || getDouble() == (double)getLong(); + } } diff --git a/src/main/java/carpet/script/value/Value.java b/src/main/java/carpet/script/value/Value.java index 91e3e893..c2f188e7 100644 --- a/src/main/java/carpet/script/value/Value.java +++ b/src/main/java/carpet/script/value/Value.java @@ -13,10 +13,12 @@ import java.util.regex.PatternSyntaxException; public abstract class Value implements Comparable, Cloneable { - public static Value FALSE = new NumericValue(0); - public static Value TRUE = new NumericValue(1); - public static Value ZERO = FALSE; - public static Value NULL = new NullValue(); + public static NumericValue FALSE = new NumericValue(0); + public static NumericValue TRUE = new NumericValue(1); + public static NumericValue ZERO = FALSE; + public static NumericValue ONE = TRUE; + + public static NullValue NULL = new NullValue(); public String boundVariable; @@ -77,6 +79,10 @@ public abstract class Value implements Comparable, Cloneable public Value add(Value o) { String lstr = this.getString(); + if (o instanceof FormattedTextValue) + { + return FormattedTextValue.combine(this, o); + } if (lstr == null) // null return new StringValue(o.getString()); String rstr = o.getString(); diff --git a/src/main/resources/assets/carpet/scripts/ai_tracker.sc b/src/main/resources/assets/carpet/scripts/ai_tracker.sc index 4e7e9fd0..2e1977e3 100644 --- a/src/main/resources/assets/carpet/scripts/ai_tracker.sc +++ b/src/main/resources/assets/carpet/scripts/ai_tracker.sc @@ -145,9 +145,8 @@ global_functions = { [10+half_width,10+height,10+half_width], 0x65432100, 'buddy detection', false ); - [x,y,z] = pos(e); current_id = e~'id'; - buddies = entity_area('villager', x, y+height/2, z, 10+half_width, 10+height/2, 10+half_width); + buddies = entity_area('villager', e, 10, 10, 10); nb = length(buddies); for (filter(buddies, _~'id' != current_id), visuals+=['line', global_duration, 'from', pos(e), 'to', pos(_), 'color', 0xffff00ff]; @@ -518,9 +517,8 @@ __tick_tracker() -> return() ); p = player(); - [px, py, pz] = pos(p); in_dimension(p, - for (entity_area('valid', px, py, pz, global_range, global_range, global_range), + for (entity_area('valid', p, global_range, global_range, global_range), __handle_entity(_) ) ); diff --git a/src/main/resources/assets/carpet/scripts/distance_beta.sc b/src/main/resources/assets/carpet/scripts/distance_beta.sc index b1b3bc6d..34acbd4f 100644 --- a/src/main/resources/assets/carpet/scripts/distance_beta.sc +++ b/src/main/resources/assets/carpet/scripts/distance_beta.sc @@ -1,91 +1,155 @@ import('math','_euclidean','_manhattan','_round'); +global_display_modes = ['chat', 'line', 'centered_sphere', 'box']; + __config()->{ 'commands'->{ 'from '->'set_start', - 'from to '->'_calculate', - 'to '->_(end_pos)->(_calculate(null,end_pos)), - 'toggle_line'->'_toggle_line' + 'from to '-> _(start, end) -> (global_current_start = start; calculate(end)), + 'to '-> 'calculate', + 'clear' -> _() -> global_display_shapes = [], + 'clear last' -> _() -> if(global_display_shapes, delete(global_display_shapes, -1)), + 'display ' -> 'set_mode', + 'assist ' -> 'set_assist', + 'assist none' -> _() -> set_assist(null), }, 'arguments' -> { 'start' -> {'type' -> 'location'}, - 'end' -> {'type' -> 'location'} + 'end' -> {'type' -> 'location'}, + 'mode' -> {'type' -> 'term', 'options' -> global_display_modes }, + 'assist' -> {'type' -> 'block', 'suggest' -> ['brown_carpet']}, } }; -global_start=null; -global_end=null; +global_current_start = null; +global_display_shapes = []; +global_renderer_running = false; +global_current_mode = 'chat'; -global_show_line=false; +global_assist_block = null; + +set_assist(block) -> +( + + if (block == null, + global_assist_block = null; + handle_event('player_places_block', null); + handle_event('player_uses_item', null); + + , + global_assist_block = str(block); + handle_event('player_places_block', 'on_player_places_block'); + handle_event('player_uses_item', 'on_player_uses_item'); + ) +); + +global_generators = { + 'line' -> _(from, to) -> [ + ['line', 'from',from,'to',to] + ], + 'centered_sphere' -> _(from, to) -> [ + ['line', 'from',from,'to',to, 'line', 5, 'color', 0x88888888], + ['sphere', 'center',from,'radius',_euclidean(from,to)] + ], + 'box' -> _(from, to) -> [ + ['line', 'from',from,'to',to, 'line', 5, 'color', 0x88888888], + ['box', 'from',from,'to',to] + ], +}; + +_create_shapes(mode, from, to) -> +( + shapes = call(global_generators:mode, from+[0, 0.07, 0], to+[0, 0.07, 0]); + shapes += ['label', 'pos',to+[0,0.2,0], 'align', 'right', 'indent', -1.5, 'text',format('rb Cylindrical:')]; + shapes += ['label', 'pos',to+[0,0.2,0], 'align', 'left', 'text',_round(_euclidean(from, [to:0, from:1, to:2]), 0.001)]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 1, 'align', 'right', 'indent', -1.5, 'text',format('rb Manhattan:')]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 1, 'align', 'left', 'text',_round(_manhattan(from,to),0.001)]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 2, 'align', 'right', 'indent', -1.5, 'text',format('rb Euclidian:')]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 2, 'align', 'left', 'text',_round(_euclidean(from,to),0.001)]; + map(shapes, put(_, 1, [20, 'player', player()], 'extend'); _); +); __mktp(pos) -> str('!/tp %.6f %.6f %.6f', pos); set_start(pos)->( - global_start = pos; - global_end = null; + global_current_start = pos; print(player(),format('gi Initial point set to: ', 'g '+pos, __mktp(pos))) ); -_toggle_line()->( - global_show_line= !global_show_line; - if(global_show_line, - print('Toggled lines on'); - _draw_line() - , // else - print('Toggled lines off') - ); -); - -_calculate(start_pos, end_pos)->( - if(start_pos!=null, - set_start(start_pos); - calculate(end_pos) - , // else - if(global_start, - calculate(end_pos), - print(player(),format('r There was no initial point for '+player())); - set_start(end_pos) - ) +set_mode(mode)->( + if (global_display_modes ~ mode, + global_current_mode = mode ) ); -__on_player_places_block(player, item_tuple, hand, block) ->( - if(block=='brown_carpet' && system_info('world_carpet_rules'):'carpets'=='true', +calculate(end)->( + if(global_current_start != null, + measure(end) + , // else + print(player(),format('r No initial point selected')); + ) +); + +on_player_places_block(player, item_tuple, hand, block) -> +( + if(block==global_assist_block, effective_pos = pos(block)+[0.5, 0, 0.5]; - if(global_start==null||player~'sneaking',//wont complain for first carpet - set_start(effective_pos), - _calculate(null, effective_pos) + if(global_current_start==null||player~'sneaking',//wont complain for first carpet + set_start(effective_pos) + , // else + calculate(effective_pos) ) ) ); -calculate(end_pos)->( - global_end = end_pos; - manhattan = _round(_manhattan(global_start, global_end),0.001);//rounding to nearest 0.01 for precision, but not ridiculous decimal places - spherical = _round(_euclidean(global_start, global_end), 0.001); - //useful for chunk distances etc. - cylindrical = _round(_euclidean(global_start, [global_end:0, global_start:1, global_end:2]), 0.001); - - - print(player(),format( - 'w Distance between ', - 'c '+str('[%.1f, %.1f, %.1f]', global_start), __mktp(global_start), - 'w and ', - 'c '+str('[%.1f, %.1f, %.1f]', global_end), __mktp(global_end), 'w :\n', - 'w - Spherical: ', 'wb '+spherical+'\n', - 'w - Cylindrical: ', 'wb '+cylindrical+'\n', - 'w - Manhattan: ', 'wb '+manhattan - )); - - schedule(0,'_draw_line'); -); - -_draw_line()->( - if(global_show_line,//It will always draw from the global positions, so it will turn on if there is a line - if ( global_start && global_end, - draw_shape('line',20,'from',global_start,'to',global_end, 'player',player()); - draw_shape('label',20,'pos',global_end,'text',_round(_euclidean(global_start,global_end),0.001)); - ); - schedule(20,'_draw_line') +on_player_uses_item(player, item_tuple, hand) -> +( + if (item_tuple:0 == global_assist_block && hand == 'mainhand', + if (player ~'sneaking', + global_current_mode = global_display_modes:(global_display_modes ~ global_current_mode + 1); + display_title(player, 'actionbar', format('w Distance mode: ','e '+global_current_mode)); + , + if(global_display_shapes, delete(global_display_shapes, -1)) + ) ) ); + +measure(end)-> +( + if (global_current_start == null, return()); + start = global_current_start; + if (global_current_mode == 'chat', + manhattan = _round(_manhattan(start, end),0.001);//rounding to nearest 0.01 for precision, but not ridiculous decimal places + spherical = _round(_euclidean(start, end), 0.001); + //useful for chunk distances etc. + cylindrical = _round(_euclidean(start, [end:0, start:1, end:2]), 0.001); + + print(player(),format( + 'w Distance between ', + 'c '+str('[%.1f, %.1f, %.1f]', start), __mktp(start), + 'w and ', + 'c '+str('[%.1f, %.1f, %.1f]', end), __mktp(end), 'w :\n', + 'w - Spherical: ', 'wb '+spherical+'\n', + 'w - Cylindrical: ', 'wb '+cylindrical+'\n', + 'w - Manhattan: ', 'wb '+manhattan + )); + , // else + global_display_shapes += _create_shapes(global_current_mode, start, end); + for (global_display_shapes, draw_shape(_) ); + if (!global_renderer_running, + global_renderer_running = true; + render() + ) + ) +); + +render()->( + if (!global_display_shapes, + global_renderer_running = false; + return(); + ); + for (global_display_shapes, + draw_shape(_); + ); + schedule(20,'render') +); diff --git a/src/main/resources/assets/carpet/scripts/draw_beta.sc b/src/main/resources/assets/carpet/scripts/draw_beta.sc index ff819431..c88739d1 100644 --- a/src/main/resources/assets/carpet/scripts/draw_beta.sc +++ b/src/main/resources/assets/carpet/scripts/draw_beta.sc @@ -20,7 +20,7 @@ __config() -> { 'arguments'->{ 'center'->{'type'->'pos', 'loaded'->'true'}, 'radius'->{'type'->'int', 'suggest'->[], 'min'->0},//to avoid default suggestions - 'replacement'->{'type'->'block'}, + 'replacement'->{'type'->'blockpredicate'}, 'height'->{'type'->'int', 'suggest'->[],'min'->0}, 'orientation'->{'type'->'term', 'suggest'->['x','y','z']}, 'pointing'->{'type'->'term','suggest'->['up','down']}, @@ -32,9 +32,20 @@ __config() -> { //"Boilerplate" code +_block_matches(existing, block_predicate) -> +( + [name, block_tag, properties, nbt] = block_predicate; + + (name == null || name == existing) && + (block_tag == null || block_tags(existing, block_tag)) && + all(properties, block_state(existing, _) == properties:_) && + (!tag || tag_matches(block_data(existing), tag)) +); + set_block(x, y, z, block, replacement)-> ( - if(block != block(x, y, z), - without_updates( if (set([x, y, z],block), + existing = block(x, y, z); + if(block != existing && (replacement && _block_matches(existing, replacement) ), + without_updates( if (set(existing,block), global_affected += [x, y, z]; )); ); diff --git a/src/main/resources/assets/carpet/scripts/event_test.sc b/src/main/resources/assets/carpet/scripts/event_test.sc index 3c6c989e..11842f56 100644 --- a/src/main/resources/assets/carpet/scripts/event_test.sc +++ b/src/main/resources/assets/carpet/scripts/event_test.sc @@ -292,3 +292,14 @@ __on_player_picks_up_item(player, item) -> print(' Player '+player+' injects '+item); ); +__on_player_trades(player, entity, buy_left, buy_right, sell) -> +( + print(''); + print('__on_player_trades(player, item)'); + print('player trades:'); + print(' - player: '+player); + print(' - entity: '+ entity); + print(' - buy_left: '+ buy_left); + print(' - buy_right: '+ buy_right); + print(' - sell: '+ sell) +); diff --git a/src/main/resources/assets/carpet/scripts/overlay.sc b/src/main/resources/assets/carpet/scripts/overlay.sc index a78aa78f..ed05761e 100644 --- a/src/main/resources/assets/carpet/scripts/overlay.sc +++ b/src/main/resources/assets/carpet/scripts/overlay.sc @@ -36,22 +36,15 @@ __config() -> 'slime_chunks' -> ['__toggle', 'slime_chunks', 'chunks'], 'portal coordinates' -> ['__toggle', 'coords', 'portals'], 'portal links' -> ['__toggle', 'links', 'portals'], - ' following ' -> ['display_shape', [null,0xffffffff], true], - ' at ' -> ['display_shape', [null,0xffffffff], false], - ' following ' -> ['display_shape', true], - ' at ' -> ['display_shape', false], + ' following ' -> ['display_shape', [null,0xffffffff], true], + ' at ' -> ['display_shape', [null,0xffffffff], false], + ' following ' -> ['display_shape', true], + ' at ' -> ['display_shape', false], ' clear' -> 'clear_shape', 'clear' -> 'clear', }, 'arguments' -> { - 'structure' -> {'type' -> 'term', 'suggest' -> [ - 'monument', 'fortress', 'mansion', 'jungle_pyramid', - 'desert_pyramid', 'endcity', 'igloo', 'shipwreck', - 'swamp_hut', 'stronghold', 'ocean_ruin', 'buried_treasure', - 'pillager_outpost', 'mineshaft', 'village', 'nether_fossil', - 'bastion_remnant', 'ruined_portal' - ]}, - 'player' -> {'type' -> 'players', 'single' -> true}, + 'structure' -> {'type' -> 'term', 'suggest' -> plop():'structures' }, 'radius' -> {'type' -> 'int', 'min' -> 0, 'max' -> 1024, 'suggest' -> [128, 24, 32]}, 'shape' -> {'type' -> 'term', 'options' -> keys(global_shapes) }, 'color' -> {'type' -> 'teamcolor'} @@ -59,29 +52,30 @@ __config() -> }; -display_shape(shape, r, player, color, following) -> +display_shape(shape, r, entities, color, following) -> ( - - player = player(player); - if (!player, return()); - if (shape == 'sphere', - shape_config = { - 'center' -> if(following, [0,0,0], pos(player)), - 'radius' -> r - } - , shape == 'box', - shape_config = { - 'from' -> if(following, [-r,-r,-r], pos(player)-r), - 'to' -> if(following, [r,r,r], pos(player)+r) - } - ); - if (shape_config, - if (following, shape_config:'follow' = player); - shape_config:'fill' = color:1 - 200; - shape_config:'color' = color:1; - shape_config:'line' = 5; - draw_shape(shape, 72000, shape_config); - global_shapes:shape += shape_config; + thicc = max(1, floor(6-length(entities)/2)); + fillc = 250 - 100/(length(entities)+1); + for (entities, + if (shape == 'sphere', + shape_config = { + 'center' -> if(following, [0,0,0], pos(_)), + 'radius' -> r + } + , shape == 'box', + shape_config = { + 'from' -> if(following, [-r,-r,-r], pos(_)-r), + 'to' -> if(following, [r,r,r], pos(_)+r) + } + ); + if (shape_config, + if (following, shape_config:'follow' = _); + shape_config:'fill' = color:1 - fillc; + shape_config:'color' = color:1; + shape_config:'line' = thicc; + draw_shape(shape, 72000, shape_config); + global_shapes:shape += shape_config; + ) ) ); diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json index 24a94b8f..2df590c4 100644 --- a/src/main/resources/carpet.mixins.json +++ b/src/main/resources/carpet.mixins.json @@ -166,11 +166,12 @@ "PlayerInventory_scarpetEventMixin", "ScoreboardObjective_scarpetMixin", "Scoreboard_scarpetMixin", + "TradeOutputSlot_scarpetEventMixin", "ChunkTicketManager_scarpetMixin", "StatType_scarpetMixin", "PointOfInterest_scarpetMixin", - "ServerWorld_scarpetEventMixin", + "ServerWorld_scarpetMixin", "HungerManagerMixin_scarpetEventsMixin", "RecipeManager_scarpetMixin", diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 182e7e0e..be2c43ff 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "id": "carpet", - "version": "1.4.19", + "version": "1.4.21", "name": "Carpet Mod in Fabric", "description": "Carpet made out of fabric",