diff --git a/build.gradle b/build.gradle index b7e5897c..2dc45583 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'fabric-loom' version '0.2.6-SNAPSHOT' + id 'fabric-loom' version '0.4-SNAPSHOT' id 'maven-publish' } diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md index 572c8123..22deab0d 100644 --- a/docs/scarpet/Full.md +++ b/docs/scarpet/Full.md @@ -276,7 +276,8 @@ global_foo = floor(rand(10)); check_foo_not_zero(); ... check_foo_not_zero() -> if(global_foo == 0, global_foo = 1) -# Variables and Constants + +# Variables and Constants `scarpet` provides a number of constants that can be used literally in scripts @@ -688,15 +689,22 @@ number('3bar') => null number('2')+number('2') => 4 -### `str(expr, params? ... ), str(expr, param_list)` +### `str(expr)`,`str(expr, params? ... )`, `str(expr, param_list)` -Returns a formatted string representing expression. Accepts formatting style accepted by `String.format`. -Supported types (with `"%?"` syntax): +If called with one argument, returns string representation of such value. + +Otherwise, returns a formatted string representing the expression. Arguments for formatting can either be provided as + each consecutive parameter, or as a list which then would be the only extra parameter. To format one list argument + , you can use `str(list)`, or `str('foo %s', l(list))`. + +Accepts formatting style accepted by `String.format`. +Supported types (with `"%"` syntax): * `d`, `o`, `x`: integers, octal, hex * `a`, `e`, `f`, `g`: floats * `b`: booleans * `s`: strings +* `%%`: '%' character
 str(null) => null
@@ -782,6 +790,18 @@ Returns true if task has completed, or false otherwise.
 
 Evaluates `expression` synchronized with respect to the lock `lock`. Returns the value of the expression.
 
+### `task_dock(expr)`
+
+In a not-task (running regular code on the main game thread) it is a pass-through command. In tasks - it docks
+the current thread on the main server thread and executes expression as one server offline server task.
+This is especially helpful in case a task has several docking operations to perform, such as setting a block, and
+it would be much more efficient to do them all at once rather then packing each block access in each own call.
+
+Be mindful, that docking the task means that the tick execution will be delayed until the expression is evaluated.
+This will synchronize your task with other tasks using `task_dock`, but if you should be using `synchronize` to
+synchronize tasks without locking the main thread.
+
+
 * * *
 
 ## Auxiliary functions
@@ -886,8 +906,11 @@ sleep(50)
 
 ### `time()`
 
-Returns the number of milliseconds since 'some point', like Java's `System.nanoTime()`. 
-It returns a float, which has 1 microsecond precision (0.001 ms)
+Returns the number of milliseconds since 'some point', like Java's `System.nanoTime()`, which varies from system to 
+system and from Java to Java. This measure should NOT be used to determine the current (date)time, but to measure
+durations of things.
+it returns a float with time in milliseconds (ms) for convenience and microsecond (μs) resolution for sanity.
+
 
 
 start_time = time();
@@ -1606,8 +1629,8 @@ set(x,y,z,'hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:
 
 Evaluates subexpression without causing updates when blocks change in the world
 
-Consider following scenario: We would like to generate a bunch of terrain in a flat world following a perling noise 
-generator. The following code causes cascading effect as blocks placed on chunk borders will cause other chunks to get 
+Consider following scenario: We would like to generate a bunch of terrain in a flat world following a perlin noise 
+generator. The following code causes a cascading effect as blocks placed on chunk borders will cause other chunks to get 
 loaded to full, thus generated:
 
 
@@ -1621,7 +1644,7 @@ __on_chunk_generated(x, z) -> (
 )
 
-The following addition resolves this issue, by not allowing block updates pass chunk borders: +The following addition resolves this issue, by not allowing block updates past chunk borders:
 __config() -> m(l('scope', 'global'));
@@ -1638,19 +1661,26 @@ __on_chunk_generated(x, z) -> (
 
 ### `place_item(item, pos, facing?, sneak?)`
 
-Places a given item in the world like it was placed by a player. Item names are default minecraft item name, 
+Uses a given item in the world like it was used by a player. Item names are default minecraft item name, 
 less the minecraft prefix. Default facing is 'up', but there are other options: 'down', 'north', 'east', 'south', 
 'west', but also there are other secondary directions important for placement of blocks like stairs, doors, etc. 
 Try experiment with options like 'north-up' which is placed facing north with cursor pointing to the upper part of the 
 block, or 'up-north', which means a block placed facing up (player looking down) and placed smidge away of the block 
 center towards north. Optional sneak is a boolean indicating if a player would be sneaking while placing the 
-block - this option only affects placement of chests and scaffolding at the moment. Returns true if placement was 
+block - this option only affects placement of chests and scaffolding at the moment. 
+
+Works with items that have the right-click effect on the block placed, like `bone_meal` on grass or axes on logs,
+but doesn't open chests / containers, so have no effect on interactive blocks, like TNT, comparators, etc.
+
+Returns true if placement/use was 
 successful, false otherwise.
 
 
 place_item('stone',x,y,z) // places a stone block on x,y,z block
 place_item('piston,x,y,z,'down') // places a piston facing down
 place_item('carrot',x,y,z) // attempts to plant a carrot plant. Returns true if could place carrots at that position.
+place_item('bone_meal',x,y,z) // attempts to bonemeal the ground.
+place_item('wooden_axe',x,y,z) // attempts to strip the log.
 
### `set_poi(pos, type, occupancy?)` @@ -1663,9 +1693,10 @@ If type is `null`, POI at position is removed. In any case, previous POI is also Interestingly, `unemployed`, and `nitwit` are not used in the game, meaning, they could be used as permanent spatial markers for scarpet apps. `meeting` is the only one with increased max occupancy of 32. -### `set_biome(pos, biome_name)` +### `set_biome(pos, biome_name, update=true)` -changes biome at that block position. +Changes the biome at that block position. if update is specified and false, then chunk will not be refreshed +on the clients. Biome changes can only be send to clients with the entire data from the chunk. ### `update(pos)` @@ -1681,7 +1712,7 @@ Causes a random tick at position. ### `destroy(pos), destroy(pos, -1), destroy(pos, ), destroy(pos, tool, nbt?)` -Destroys the block like it was mined by a player. Add -1 for silk touch, and positive number for fortune level. +Destroys the block like it was mined by a player. Add -1 for silk touch, and a positive number for fortune level. If tool is specified, and optionally its nbt, it will use that tool and will attempt to mine the block with this tool. If called without item context, this function, unlike harvest, will affect all kinds of blocks. If called with item in context, it will fail to break blocks that cannot be broken by a survival player. @@ -1697,28 +1728,28 @@ for mining. Obviously, in this case the use of `harvest` would be much more appl
 mine(x,y,z) ->
 (
-   p = player();
-   slot = p~'selected_slot';
-   item_tuple = inventory_get(p, slot);
-   if (!item_tuple, destroy(x,y,z,'air'); return()); // empty hand, just break with 'air'
-   l(item, count, tag) = item_tuple;
-   tag_back = destroy(x,y,z, item, tag);
-   if (tag_back == false, // failed to break the item
-	     return(tag_back)
-   );
-   if (tag_back == true, // block broke, tool has no tag
-	     return(tag_back)
-   );
-   if (tag_back == null, //item broke
-	     delete(tag:'Damage');
-	     inventory_set(p, slot, count-1, item, tag);
-	     return(tag_back)
-   );
-   if (type(tag_back) == 'nbt', // item didn't break, here is the effective nbt
-	     inventory_set(p, slot, count, item, tag_back);
-	     return(tag_back)
-   );
-   print('How did we get here?');
+  p = player();
+  slot = p~'selected_slot';
+  item_tuple = inventory_get(p, slot);
+  if (!item_tuple, destroy(x,y,z,'air'); return()); // empty hand, just break with 'air'
+  l(item, count, tag) = item_tuple;
+  tag_back = destroy(x,y,z, item, tag);
+  if (tag_back == false, // failed to break the item
+    return(tag_back)
+  );
+  if (tag_back == true, // block broke, tool has no tag
+    return(tag_back)
+  );
+  if (tag_back == null, //item broke
+    delete(tag:'Damage');
+    inventory_set(p, slot, count-1, item, tag);
+    return(tag_back)
+  );
+  if (type(tag_back) == 'nbt', // item didn't break, here is the effective nbt
+    inventory_set(p, slot, count, item, tag_back);
+    return(tag_back)
+  );
+  print('How did we get here?');
 )
 
@@ -1736,9 +1767,9 @@ Returns a triple of coordinates of a specified block or entity. Technically enti and the same can be achieved with `query(entity,'pos')`, but for simplicity `pos` allows to pass all positional objects.
-pos(block(0,5,0))  => l(0,5,0)
+pos(block(0,5,0)) => l(0,5,0)
 pos(players()) => l(12.3, 45.6, 32.05)
-pos(block('stone'))  => Error: Cannot fetch position of an unrealized block
+pos(block('stone')) => Error: Cannot fetch position of an unrealized block
 
### `pos_offset(pos, direction, amount?)` @@ -1796,51 +1827,51 @@ poi(x,y,z,5) => [['nether_portal',0,[7,8,9]],['nether_portal',0,[7,9,9]]] // two ### `biome(pos)` -returns biome at that block position. +Returns the biome at that block position. ### `solid(pos)` -Boolean function, true if the block is solid +Boolean function, true if the block is solid. ### `air(pos)` -Boolean function, true if a block is air.... or cave air... or void air.... or any other air they come up with. +Boolean function, true if a block is air... or cave air... or void air... or any other air they come up with. ### `liquid(pos)` -Boolean function, true if the block is liquid, or liquidlogged +Boolean function, true if the block is liquid, or waterlogged (with any liquid). ### `flammable(pos)` -Boolean function, true if the block is flammable +Boolean function, true if the block is flammable. ### `transparent(pos)` -Boolean function, true if the block is transparent +Boolean function, true if the block is transparent. ### `opacity(pos)` -Numeric, returning opacity level of a block +Numeric function, returning the opacity level of a block. ### `blocks_daylight(pos)` -Boolean function, true if the block blocks daylight +Boolean function, true if the block blocks daylight. ### `emitted_light(pos)` -Numeric, returning light level emitted from block +Numeric function, returning the light level emitted from the block. ### `light(pos)` -Integer function, returning total light level at position +Numeric function, returning the total light level at position. ### `block_light(pos)` -Integer function, returning block light at position. From torches and other light sources. +Numeric function, returning the block light at position (from torches and other light sources). ### `sky_light(pos)` -Numeric function, returning sky light at position. From the sky access. +Numeric function, returning the sky light at position (from sky access). ### `see_sky(pos)` @@ -1888,7 +1919,7 @@ Boolean function, true if the block ticks randomly. ### `blocks_movement(pos)` -Boolean function, true if block at position blocks movement. +Boolean function, true if the block at position blocks movement. ### `block_sound(pos)` @@ -2014,11 +2045,11 @@ Returns the map colour of a block at position. One of: ### `loaded(pos)` Boolean function, true if the block is accessible for the game mechanics. Normally `scarpet` doesn't check if operates -on loaded area - the game will automatically load missing blocks. We see this as advantage. Vanilla `fill/clone` +on loaded area - the game will automatically load missing blocks. We see this as an advantage. Vanilla `fill/clone` commands only check the specified corners for loadness. -To check if block is truly loaded, I mean in memory, use `generation_status(x) != null`, as chunks can still be loaded -outside of the playable area, just are not used any of the game mechanics processes. +To check if a block is truly loaded, I mean in memory, use `generation_status(x) != null`, as chunks can still be loaded +outside of the playable area, just are not used by any of the game mechanic processes.
 loaded(pos(players()))  => 1
@@ -2033,7 +2064,7 @@ Deprecated as of scarpet 1.6, use `loaded_status(x) > 0`, or just `loaded(x)` wi
 
 ### `loaded_status(pos)`
 
-Returns loaded status as per new 1.14 chunk ticket system, 0 for inaccessible, 1 for border chunk, 2 for ticking, 
+Returns loaded status as per new 1.14 chunk ticket system, 0 for inaccessible, 1 for border chunk, 2 for redstone ticking, 
 3 for entity ticking
 
 ### `is_chunk_generated(pos)`, `is_chunk_generated(pos, force)`
@@ -2055,13 +2086,41 @@ Returns generation status as per the ticket system. Can return any value from se
 can only be stable in a few states: `full`, `features`, `liquid_carvers`, and `structure_starts`. Returns `null` 
 if the chunk is not in memory unless called with optional `true`.
 
+### `inhabited_time(pos)`
+
+Returns inhabited time for a chunk.
+
+### `spawn_potential(pos)`
+
+Returns spawn potential at a location (1.16+ only)
+
+### `structure_eligibility(pos, ?structure, ?size_needed)`
+
+Checks wordgen eligibility for a structure in a given chunk. If no structure is given, or `null`, then it will check
+ for all structures. If bounding box of the structures is also requested, it will compute size of potential
+  structures. This function, unlike other in the `structure*` category is not using world data nor accesses chunks
+  making it preferred for scoping ungenerated terrain, but it takes some compute resources to calculate the structure.
+  
+  Unlike `'structure'` this will return a tentative structure location. Random factors in world generation may prevent
+  the actual structure from forming.
+  
+If structure is specified, it will return `null` if a chunk is not eligible, `true` if the structure should appear, or 
+a map with two values: `'box'` for a pair of coordinates indicating bounding box of the structure, and `'pieces'` for 
+list of elements of the structure (as a tuple), with its name, direction, and box coordinates of the piece.
+
+If structure is not specified, it will return a set of structure names that are eligible, or a map with structures
+as keys, and same type of map values as with a single structure call. An empty set or an empty map would indicate that nothing
+should be generated there.
+
+
 ### `structures(pos), structures(pos, structure_name)`
 
 Returns structure information for a given block position. Note that structure information is the same for all the 
 blocks from the same chunk. `structures` function can be called with a block, or a block and a structure name. In 
 the first case it returns a map of structures at a given position, keyed by structure name, with values indicating 
 the bounding box of the structure - a pair of two 3-value coords (see examples). When called with an extra structure 
-name, returns list of components for that structure, with their name, direction and two sets of coordinates 
+name, returns a map with two values, `'box'` for bounding box of the structure, and `'pieces'` for a list of 
+components for that structure, with their name, direction and two sets of coordinates 
 indicating the bounding box of the structure piece.
 
 ### `structure_references(pos), structure_references(pos, structure_name)`
@@ -2076,7 +2135,7 @@ then to get its bounding boxes.
 
 Creates or removes structure information of a structure associated with a chunk of `pos`. Unlike `plop`, blocks are 
 not placed in the world, only structure information is set. For the game this is a fully functional structure even 
-if blocks are not set. To remove structure a given point is in, use `structure_references` to find where current 
+if blocks are not set. To remove the structure a given point is in, use `structure_references` to find where current 
 structure starts.
 
 ### `plop(pos, what)`
@@ -2122,6 +2181,13 @@ Structure list:
 *   `village_savanna`: Savanna, acacia village.
 *   `village_taiga`: Taiga, spruce village.
 *   `village_snowy`: Resolute, Canada.
+*   `nether_fossil`: Pile of bones (1.16)
+*   `ruined_portal`: Ruined portal, random variant.
+*   `bastion_remnant`: Piglin bastion, random variant for the chunk (1.16)
+*   `bastion_remnant_housing`: Housing units version of a piglin bastion (1.16)
+*   `bastion_remnant_stable`: Hoglin stables version of q piglin bastion (1.16)
+*   `bastion_remnant_treasure`: Treasure room version of a piglin bastion (1.16)
+*   `bastion_remnant_bridge` : Bridge version of a piglin bastion (1.16)
 
 Feature list:
 
@@ -2178,6 +2244,18 @@ Feature list:
 *   `coral`: random coral structure. Require water to spawn.
 *   `sea_pickle`
 *   `boulder`: A rocky, mossy formation from a giant taiga biome. Doesn't update client properly, needs relogging.
+*   `crimson_fungus` (1.16)
+*   `warped_fungus` (1.16)
+*   `nether_sprouts` (1.16)
+*   `crimson_roots` (1.16)
+*   `warped_roots`  (1.16)
+*   `weeping_vines` (1.16)
+*   `twisting_vines` (1.16)
+*   `basalt_pillar` (1.16)
+
+### `reload_chunk(pos)`
+
+Sends full chunk data to clients. Useful when lots stuff happened and you want to refresh it on the clients.
 
 ### `reset_chunk(pos)`, `reset_chunk(from_pos, to_pos)`, `reset_chunk(l(pos, ...))`
 Removes and resets the chunk, all chunks in the specified area or all chunks in a list at once, removing all previous
@@ -2193,15 +2271,25 @@ It returns a `map` with a report indicating how many chunks were affected, and h
  * `layer_count_`: number of chunks for which a `` generation step has been performed
  * `layer_time_`: cumulative time for all chunks spent on generating `` step
  
-# Iterating over larger areas of blocks
+### add_chunk_ticket(pos, type, radius)
+
+Adds a chunk ticket at a position, which makes the game to keep the designated area centered around
+`pos` with radius of `radius` loaded for a predefined amount of ticks, defined by `type`. Allowed types
+are `portal`: 300 ticks, `teleport`: 5 ticks, and `unknown`: 1 tick. Radius can be from 1 to 32 ticks.
+
+This function is tentative - will likely change when chunk ticket API is properly fleshed out.# Iterating over larger areas of blocks
 
 These functions help scan larger areas of blocks without using generic loop functions, like nested `loop`.
 
-### `scan(cx, cy, cz, dx, dy, dz, px?, py?, pz?, expr)`
+### `scan(cx, cy, cz, dx, dy, dz, px?, py?, pz?, expr)`, `scan(center, range, lower_range?, expr)`
 
-Evaluates expression over area of blocks defined by its center (`cx, cy, cz`), expanded in all directions 
-by `dx, dy, dz` blocks, or optionally in negative with `d` coords, and `p` coords in positive values. `expr` 
-receives `_x, _y, _z` as coords of current analyzed block and `_`, which represents the block itself.
+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
+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, 
 which would cause the expression not be evaluated for their boolean value.
@@ -2211,7 +2299,7 @@ return value. `break` return value has no effect.
 
 ### `volume(x1, y1, z1, x2, y2, z2, expr)`
 
-Evaluates expression for each block in the area, the same as the `scan`function, but using two opposite corners of 
+Evaluates expression for each block in the area, the same as the `scan` function, but using two opposite corners of 
 the rectangular cuboid. Any corners can be specified, its like you would do with `/fill` command.
 
 For return value and handling `break` and `continue` statements, see `scan` function above.
@@ -2227,7 +2315,7 @@ for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block
 ### `rect(cx, cy, cz, dx?, dy?, dz?, px?, py?, pz?)`
 
 returns an iterator, just like `range` function that iterates over rectangular cubarea of blocks. If only center 
-point is specified, it iterates over 27 blocks. If `d` arguments are specified, expands selection of respective 
+point is specified, it iterates over 27 blocks. If `d` arguments are specified, expands selection by the  respective 
 number of blocks in each direction. If `p` arguments are specified, it uses `d` for negative offset, and `p` for positive.
 
 ### `diamond(cx, cy, cz, radius?, height?)`
@@ -2240,23 +2328,23 @@ Any of these can be zero as well. radius of 0 makes a stick, height of 0 makes a
 ## Entity Selection
 
 Entities have to be fetched before using them. Entities can also change their state between calls to the script if 
-game happens either in between separate calls to the programs, or if the program calls `game_tick` on its own. 
-In this case - entities would need to be re-fetched, or the code should account for entities getting dead.
+game ticks occur either in between separate calls to the programs, or if the program calls `game_tick` on its own. 
+In this case - entities would need to be re-fetched, or the code should account for entities dying.
 
 ### `player(), player(type), player(name)`
 
 With no arguments, it returns the calling player or the player closest to the caller. Note that the main context 
-will receive `p` variable pointing to this player. With `type` or `name` specified it will try first to match a type, 
+will receive `p` variable pointing to this player. With `type` or `name` specified, it will try first to match a type, 
 returning a list of players matching a type, and if this fails, will assume its player name query retuning player with 
 that name, or `null` if no player was found. With `'all'`, list of all players in the game, in all dimensions, so end 
 user needs to be cautious, that you might be referring to wrong blocks and entities around the player in question. 
 With `type = '*'` it returns all players in caller dimension, `'survival'` returns all survival and adventure players,
 `'creative'` returns all creative players, `'spectating'` returns all spectating players, and `'!spectating'`, 
-all not-spectating players. If all fails, with `name`, the player in question, if is logged in.
+all not-spectating players. If all fails, with `name`, the player in question, if he/she is logged in.
 
 ### `entity_id(uuid), entity_id(id)`
 
-Fetching entities wither by their ID obtained via `entity ~ 'id'`, which is unique for a dimension and current world 
+Fetching entities either by their ID obtained via `entity ~ 'id'`, which is unique for a dimension and current world 
 run, or by UUID, obtained via `entity ~ 'uuid'`. It returns null if no such entity is found. Safer way to 'store' 
 entities between calls, as missing entities will be returning `null`. Both calls using UUID or numerical ID are `O(1)`, 
 but obviously using UUIDs takes more memory and compute.
@@ -2275,12 +2363,12 @@ selectors are available:
 ### `entity_area(type, cx, cy, cz, dx, dy, dz)`
 
 Returns entities of a specified type in an area centered on `cx, cy, cz` and at most `dx, dy, dz` blocks away from 
-the center point. Uses same selectors as `entities_list`
+the center point. Uses the same selectors as `entities_list`.
 
 ### `entity_selector(selector)`
 
-Returns entities satisfying given vanilla entity selector. Most complex among all the methods of selecting entities, 
-but the most capable. Selectors are cached so should be as fast as other methods of selecting entities.
+Returns entities satisifying given vanilla entity selector. Most complex among all the methods of selecting entities, 
+but the most capable. Selectors are cached so it should be as fast as other methods of selecting entities.
 
 ### `spawn(name, pos, nbt?)`
 
@@ -2290,168 +2378,186 @@ you get the entity back as a return value, which is swell.
 
 ## Entity Manipulation
 
-Unlike with blocks, that use plethora of vastly different querying functions, entities are queried with `query` 
-function and altered via `modify` function. Type of information needed or values to be modified are different for 
+Unlike with blocks, that use a plethora of vastly different querying functions, entities are queried with the `query` 
+function and altered via the `modify` function. Type of information needed or values to be modified are different for 
 each call.
 
 Using `~` (in) operator is an alias for `query`. Especially useful if a statement has no arguments, 
-which in this case can be radically simplified
+which in this case can be radically simplified:
 
 
 query(p, 'name') <=> p ~ 'name'     // much shorter and cleaner
 query(p, 'holds', 'offhand') <=> p ~ l('holds', 'offhand')    // not really but can be done
 
-### `query(e,'removed')` +### `query(e, 'removed')` -Boolean. True if the entity is removed +Boolean. True if the entity is removed. -### `query(e,'id')` +### `query(e, 'id')` Returns numerical id of the entity. Most efficient way to keep track of entites in a script. Ids are only unique within current game session (ids are not preserved between restarts), -and dimension (each dimension has its own ids which can overlap. +and dimension (each dimension has its own ids which can overlap). -### `query(e,'uuid')` +### `query(e, 'uuid')` -Returns UUID (unique id) of the entity. Can be used to access entities with the other vanilla commands and +Returns the UUID (unique id) of the entity. Can be used to access entities with the other vanilla commands and remains unique regardless of the dimension, and is preserved between game restarts. Apparently players cannot be -accessed via UUID, but name instead. +accessed via UUID, but should be accessed with their name instead.
 map(entities_area('*',x,y,z,30,30,30),run('kill '+query(_,'id'))) // doesn't kill the player
 
-### `query(e,'pos')` +### `query(e, 'pos')` -Triple of entity position +Triple of the entity's position -### `query(e,'location')` +### `query(e, 'location')` -Quin-tuple of entity position (x, y, and z coords), and rotation (yaw, pitch) +Quin-tuple of the entity's position (x, y, and z coords), and rotation (yaw, pitch) -### `query(e,'x'), query(e,'y'), query(e,'z')` +### `query(e, 'x'), query(e, 'y'), query(e, 'z')` -Respective entity coordinate +Respective component of entity's coordinates -### `query(e,'pitch'), query(e,'yaw')` +### `query(e, 'pitch'), query(e, 'yaw')` Pitch and Yaw or where entity is looking. -### `query(e,'look')` +### `query(e, 'look')` Returns a 3d vector where the entity is looking. -### `query(e,'motion')` +### `query(e, 'motion')` -Triple of entity motion vector, `l(motion_x, motion_y, motion_z)` +Triple of entity's motion vector, `l(motion_x, motion_y, motion_z)` -### `query(e,'motion_x'), query(e,'motion_y'), query(e,'motion_z')` +### `query(e, 'motion_x'), query(e, 'motion_y'), query(e, 'motion_z')` -Respective component of the motion vector +Respective component of the entity's motion vector -### `query(e,'name'), query(e,'custom_name'), query(e,'type')` +### `query(e, 'name'), query(e, 'display_name'), query(e, 'custom_name'), query(e, 'type')` String of entity name -
query(e,'name')  => Leatherworker
+
+query(e,'name')  => Leatherworker
 query(e,'custom_name')  => null
 query(e,'type')  => villager
 
-### `query(e,'is_riding')` +## `query(e, 'command_name')` -Boolean. True if riding another entity. +Returns a valid string to be used in commands to address an entity. Its UUID for all entities except +player, where its their name. -### `query(e,'is_ridden')` +
+run('/kill ' + e~'command_name');
+
-Boolean. True if another entity is riding it. +### `query(e, 'is_riding')` -### `query(e,'passengers')` +Boolean, true if the entity is riding another entity. + +### `query(e, 'is_ridden')` + +Boolean, true if another entity is riding it. + +### `query(e, 'passengers')` List of entities riding the entity. -### `query(e,'mount')` +### `query(e, 'mount')` Entity that `e` rides. -### `query(e,'tags')` +### `query(e, 'tags')` -List of entity tags. +List of entity's tags. -### `query(e,'has_tags',tag)` +### `query(e, 'has_tag',tag)` -Boolean, True if the entity is marked with `tag`. +Boolean, true if the entity is marked with `tag`. -### `query(e,'is_burning')` +### `query(e, 'is_burning')` -Boolean, True if the entity is burning. +Boolean, true if the entity is burning. -### `query(e,'fire')` +### `query(e, 'fire')` Number of remaining ticks of being on fire. -### `query(e,'silent')` +### `query(e, 'silent')` -Boolean, True if the entity is silent. +Boolean, true if the entity is silent. -### `query(e,'gravity')` +### `query(e, 'gravity')` -Boolean, True if the entity is affected by gravity, like most entities do. +Boolean, true if the entity is affected by gravity, like most entities are. -### `query(e,'immune_to_fire')` +### `query(e, 'immune_to_fire')` -Boolean, True if the entity is immune to fire. +Boolean, true if the entity is immune to fire. -### `query(e,'dimension')` +### `query(e, 'dimension')` -Name of the dimension entity is in. +Name of the dimension the entity is in. -### `query(e,'height')` +### `query(e, 'height')` -Height of the entity. +Height of the entity in blocks. -### `query(e,'width')` +### `query(e, 'width')` -Width of the entity. +Width of the entity in blocks. -### `query(e,'eye_height')` +### `query(e, 'eye_height')` -Eye height of the entity. +Eye height of the entity in blocks. -### `query(e,'age')` +### `query(e, 'age')` -Age, in ticks, of the entity, i.e. how long it existed. +Age of the entity in ticks, i.e. how long it existed. -### `query(e,'breeding_age')` +### `query(e, 'breeding_age')` -Breeding age of passive entity, in ticks. If negative it it time to adulthood, if positive, breeding cooldown +Breeding age of passive entity, in ticks. If negative, time to adulthood, if positive, breeding cooldown -### `query(e,'despawn_timer')` +### `query(e, 'despawn_timer')` -For living entities - the number of ticks they fall outside of immediate player presence. +For living entities, the number of ticks they fall outside of immediate player presence. -### `query(e,'item')` +### `query(e, 'portal_cooldown')` + +Number of ticks remaining until an entity can use a portal again. + +### `query(e, 'portal_timer')` + +Number of ticks an entity sits in a portal. + +### `query(e, 'item')` The item triple (name, count, nbt) if its an item entity, `null` otherwise -### `query(e,'count')` +### `query(e, 'count')` Number of items in a stack from item entity, `null` otherwise -### `query(e,'pickup_delay')` +### `query(e, 'pickup_delay')` Retrieves pickup delay timeout for an item entity, `null` otherwise -### `query(e,'is_baby')` +### `query(e, 'is_baby')` Boolean, true if its a baby. -### `query(e,'target')` +### `query(e, 'target')` Returns mob's attack target or null if none or not applicable. -### `query(e,'home')` +### `query(e, 'home')` Returns creature's home position or null if none or not applicable. @@ -2466,31 +2572,31 @@ Returns a pose of an entity, one of the following options * `'crouching'` * `'dying'` -### `query(e,'sneaking')` +### `query(e, 'sneaking')` -Boolean, true if entity is sneaking. +Boolean, true if the entity is sneaking. -### `query(e,'sprinting')` +### `query(e, 'sprinting')` -Boolean, true if entity is sprinting. +Boolean, true if the entity is sprinting. -### `query(e,'swimming')` +### `query(e, 'swimming')` -Boolean, true if entity is swimming. +Boolean, true if the entity is swimming. -### `query(e,'jumping')` +### `query(e, 'jumping')` -Boolean, true if entity is jumping. +Boolean, true if the entity is jumping. -### `query(e,'gamemode')` +### `query(e, 'gamemode')` String with gamemode, or `null` if not a player. -### `query(e,'gamemode_id')` +### `query(e, 'gamemode_id')` Good'ol gamemode id, or null if not a player. -### `query(e,'player_type')` +### `query(e, 'player_type')` Returns `null` if the argument is not a player, otherwise: @@ -2502,18 +2608,25 @@ Returns `null` if the argument is not a player, otherwise: * `shadow`: any carpet-shadowed real player * `realms`: ? -### `query(e,'ping')` +### `query(e, 'category')` +Returns a lowercase string containing the category of the entity (hostile, passive, water, ambient, misc) + +### `query(e, 'team')` + +Team name for entity, or `null` if no team is assigned. + +### `query(e, 'ping')` Player's ping in milliseconds, or `null` if its not a player. -### `query(e,'permission_level')` +### `query(e, 'permission_level')` Player's permission level, or `null` if not applicable for this entity. -### `query(e,'effect',name?)` +### `query(e, 'effect', name?)` Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short -effect name, amplifier, and remaining duration. With an argument, if the living entity has not that potion active, +effect name, amplifier, and remaining duration in ticks. With an argument, if the living entity has not that potion active, returns `null`, otherwise return a tuple of amplifier and remaining duration.
@@ -2522,13 +2635,20 @@ query(p,'effect','haste')  => [0, 177]
 query(p,'effect','resistance')  => null
 
-### `query(e,'health')` +### `query(e, 'health')` Number indicating remaining entity health, or `null` if not applicable. -### `query(e,'holds',slot?)` +### `query(e, 'hunger')` +### `query(e, 'saturation')` +### `query(e, 'exhaustion')` -Returns triple of short name, stack count, and NBT of item held in `slot`. Available options for `slot` are: +Retrieves player hunger related information. For non-players, returns `null`. + + +### `query(e, 'holds', slot?)` + +Returns triple of short name, stack count, and NBT of item held in `slot`, or `null` if nothing or not applicable. Available options for `slot` are: * `mainhand` * `offhand` @@ -2539,16 +2659,16 @@ Returns triple of short name, stack count, and NBT of item held in `slot`. Avail If `slot` is not specified, it defaults to the main hand. -### `query(e,'selected_slot')` +### `query(e, 'selected_slot')` -Number indicating the selected slot of entity inventory. Currently only applicable to players. +Number indicating the selected slot of entity's inventory. Currently only applicable to players. -### `query(e,'facing', order?)` +### `query(e, 'facing', order?)` Returns where the entity is facing. optional order (number from 0 to 5, and negative), indicating primary directions where entity is looking at. From most prominent (order 0) to opposite (order 5, or -1) -### `query(e,'trace', reach?, options?...)` +### `query(e, 'trace', reach?, options?...)` Returns the result of ray tracing from entity perspective, indicating what it is looking at. Default reach is 4.5 blocks (5 for creative players), and by default it traces for blocks and entities, identical to player attack tracing @@ -2560,9 +2680,9 @@ entities and blocks, blocks will take over the priority even if transparent or n Regardless of the options selected, the result could be `null` if nothing is in reach, entity, if look targets an entity, and block value if block is in reach. -### `query(e,'nbt',path?)` +### `query(e, 'nbt', path?)` -Returns full NBT of the entity. If path is specified, it fetches only that portion of the NBT, that corresponds to the +Returns full NBT of the entity. If path is specified, it fetches only the portion of the NBT that corresponds to the path. For specification of `path` attribute, consult vanilla `/data get entity` command. Note that calls to `nbt` are considerably more expensive comparing to other calls in Minecraft API, and should only @@ -2574,13 +2694,13 @@ type objects via `get, put, has, delete`, so try to use API calls first for that Like with entity querying, entity modifications happen through one function. Most position and movements modifications don't work currently on players as their position is controlled by clients. -Currently there is no ability to modify NBT directly, but you could always use `run('data modify entity` +Currently there is no ability to modify NBT directly, but you could always use `run('data modify entity ...')` -### `modify(e,'remove')` +### `modify(e, 'remove')` Removes (not kills) entity from the game. -### `modify(e,'kill')` +### `modify(e, 'kill')` Kills the entity. @@ -2594,7 +2714,7 @@ Changes full location vector all at once. ### `modify(e, 'x', x), modify(e, 'y', y), modify(e, 'z', z)` -Moves the entity in.... one direction. +Moves the entity in one direction. ### `modify(e, 'pitch', pitch), modify(e, 'yaw', yaw)` @@ -2616,21 +2736,35 @@ Sets the corresponding component of the motion vector. Adds a vector to the motion vector. Most realistic way to apply a force to an entity. -### `modify(e, 'custom_name'), modify(e, 'custom_name', name )` +### `modify(e, 'custom_name'), modify(e, 'custom_name', name)` + +Sets the custom name of the entity. ### `modify(e, 'age', number)` -Modifies entity's internal age counter. Fiddling with this will affect directly behaviours of complex +Modifies entity's internal age counter. Fiddling with this will affect directly AI behaviours of complex entities, so use it with caution. ### `modify(e, 'pickup_delay', number)` +Sets the pickup delay for the item entity. + ### `modify(e, 'breeding_age', number)` +Sets the breeding age for the animal. + ### `modify(e, 'despawn_timer', number)` Sets a custom despawn timer value. +### `modify(e, 'portal_cooldown', number)` + +Sets a custom number of ticks remaining until an entity can use a portal again. + +### `modify(e, 'portal_timer', number)` + +Sets a custom number of ticks an entity sits in a portal. + ### `modify(e, 'dismount')` Dismounts riding entity. @@ -2649,11 +2783,11 @@ Mounts on all listed entities on `e`. ### `modify(e, 'tag', tag, ? ...), modify(e, 'tag', l(tags) )` -Adds tag / tags to the entity. +Adds tag(s) to the entity. ### `modify(e, 'clear_tag', tag, ? ...), modify(e, 'clear_tag', l(tags) )` -Removes tag from entity. +Removes tag(s) from the entity. ### `modify(e, 'talk')` @@ -2666,18 +2800,63 @@ If called with `false` value, it will disable AI in the mob. `true` will enable ### `modify(e, 'no_clip', boolean)` Sets if the entity obeys any collisions, including collisions with the terrain and basic physics. Not affecting -players, since they are controlled client side +players, since they are controlled client side. -### `modify(e, 'effect', name, duration?, amplifier?, show_particles?, show_icon?)` +### `modify(e, 'effect', name?, duration?, amplifier?, show_particles?, show_icon?)` Applies status effect to the living entity. Takes several optional parameters, which default to `0`, `true` -and `true`. If no duration is specified, or it is null or 0, the effect is removed. +and `true`. If no duration is specified, or if it's null or 0, the effect is removed. If name is not specified, +it clears all effects. ### `modify(e, 'home', null), modify(e, 'home', block, distance?), modify(e, 'home', x, y, z, distance?)` Sets AI to stay around the home position, within `distance` blocks from it. `distance` defaults to 16 blocks. `null` removes it. _May_ not work fully with mobs that have this AI built in, like Villagers. +### `modify(e, 'gamemode', gamemode?), modify(e, 'gamemode', gamemode_id?)` + +Modifies gamemode of player to whatever string (case-insensitive) or number you put in. + +* 0: survival +* 1: creative +* 2: adventure +* 3: spectator + +### `modify(e, 'jumping', boolean)` + +Will make the entity constantly jump if set to true, and will stop the entity from jumping if set to false. +Note that jumping parameter can be fully controlled by the entity AI, so don't expect that this will have +a permanent effect. Use `'jump'` to make an entity jump once for sure. + +Requires a living entity as an argument. + +### `modify(e, 'jump'))` + +Will make the entity jump once. + +### `modify(e, 'silent', boolean)` + +Silences or unsilences the entity. + +### `modify(e, 'gravity', boolean)` + +Toggles gravity for the entity. + +### `modify(e, 'fire', ticks)` + +Will set entity on fire for `ticks` ticks. Set to 0 to extinguish. + +### `modify(e, 'hunger', value)` +### `modify(e, 'saturation', value)` +### `modify(e, 'exhaustion', value)` + +Modifies directly player raw hunger components. Has no effect on non-players + +### `modify(e, 'add_exhaustion', value)` + +adds exhaustion value to the current player exhaustion level - that's the method you probably want to use +to manipulate how much 'food' some action cost. + ## Entity Events There is a number of events that happen to entities that you can attach your own code to in the form of event handlers. @@ -2688,19 +2867,19 @@ defining the callback with `entity_event` The following events can be handled by entities. -* `'on_tick'`: executes every tick right before entity is ticked in the game. Required arguments: `entity` +* `'on_tick'`: executes every tick right before the entity is ticked in the game. Required arguments: `entity` * `'on_death'`: executes once when a living entity dies. Required arguments: `entity, reason` * `'on_removed'`: execute once when an entity is removed. Required arguments: `entity` * `'on_damaged'`: executed every time a living entity is about to receive damage. 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 will attach inapplicable event to it. +when you attach inapplicable event to it. ### `entity_event(e, event, call_name, args...)` -Attaches specific function from the current package to be called upon the `event`, with extra `args` curried to the -original required arguments for the event handler +Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the +original required arguments for the event handler.
 protect_villager(entity, amount, source, source_entity, healing_player) ->
@@ -2727,7 +2906,12 @@ health back to the villager after being harmed.
 
 Most functions in this category require inventory as the first argument. Inventory could be specified by an entity, 
 or a block, or position (three coordinates) of a potential block with inventory. Player enderchest inventory require 
-two arguments, keyword `'enderchest'`, followed by the player entity argument. If the entity or a block doesn't have 
+two arguments, keyword `'enderchest'`, followed by the player entity argument, or a single argument as a string of a
+form: `'enderchest_steve'`. If your player name starts with enderchest, it can be always accessed by passing a player
+entity value. If all else fails, it will try to identify first three arguments as coordinates of a block position of
+a block inventory. Player inventories can also be called by their name.
+ 
+ If the entity or a block doesn't have 
 an inventory, they 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. 
@@ -2858,7 +3042,7 @@ __on_tick()         // can access blocks and entities in the overworld
 __on_tick_nether()  // can access blocks and entities in the nether
 __on_tick_ender()   // can access blocks and entities in the end
 __on_chunk_generated(x,z) // called after a chunk is promoted to the full chunk,
-                          // prodiving lowest x and z coords in the chunk
+                          // providing lowest x and z coords in the chunk
                           // event will not work with optifine installed in the game
 __on_lightning(block, mode) // mode is `true` if lightning caused horse trap to spawn
 // player specific callbacks
@@ -2872,6 +3056,11 @@ __on_player_interacts_with_block(player, hand, block, face, hitvec)  //right cli
 __on_player_places_block(player, item_tuple, hand, block) // player have just placed the block.
 __on_player_interacts_with_entity(player, entity, hand)
 __on_player_attacks_entity(player, entity)
+__on_player_takes_damage(player, amount, source, source_entity)
+__on_player_deals_damage(player, amount, entity)
+__on_player_dies(player)
+__on_player_respawns(player)
+__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension)
 __on_player_rides(player, forward, strafe, jumping, sneaking)
 __on_player_jumps(player)
 __on_player_deploys_elytra(player)
@@ -2882,9 +3071,16 @@ __on_player_starts_sprinting(player)
 __on_player_stops_sprinting(player)
 __on_player_drops_item(player)
 __on_player_drops_stack(player)
+__on_player_connects(player)
+__on_player_disconnects(player, reason)
 __on_statistic(player, category, event, value) // player statistic changes
 
+Couple special cases. +* `__on_player_changes_dimension` returns `null` as `to_pos` when player goes back to the overworld from the end +, since the respawn location of the player is not control by the teleport, or a player can see the end credits. After + the player is eligible to respawn in the overworld, `__on_player_respawns` will be triggered. + ### `/script event` command used to display current events and bounded functions. use `add_to` ro register new event, or `remove_from` to @@ -2910,9 +3106,12 @@ scoreboard_add('counter') scoreboard_add('lvl','level')
-### `scoreboard_remove(objective)` +### `scoreboard_remove(objective)` `scoreboard_remove(objective, key)` -Removes an objective. Returns `true` if objective has existed and has been removed. +Removes an entire objective, or an entry in the scoreboard associated with the key. +Returns `true` if objective has existed and has been removed, or previous +value of the scoreboard if players score is removed. Returns `null` if objective didn't exist, or a key was missing +for the objective. ### `scoreboard_display(place, objective)` @@ -2923,36 +3122,112 @@ Collection of other methods that control smaller, yet still important aspects of ## Sounds -### `sound(name, pos, volume?, pitch?)` +### `sound(name, pos, volume?, pitch?, mixer?)` -Plays a specific sound `name`, at block or position `pos`, with optional `volume` and modified `pitch`. `pos` can be -either a block, triple of coords, or a list of thee numbers. Uses the same options as a corresponding `playsound` command. +Plays a specific sound `name`, at block or position `pos`, with optional `volume` and modified `pitch`, and under +optional `mixer`. Default values for `volume`, `pitch` and `mixer` are `1.0`, `1.0`, and `master`. +Valid mixer options are `master`, `music`, `record`, `weather`, `block`, `hostile`,`neutral`, `player`, `ambient` +and `voice`. `pos` can be either a block, triple of coords, or a list of thee numbers. Uses the same options as a + corresponding `playsound` command. ## Particles -### `particle(name, pos, count?. spread?, speed?, playername?)` +### `particle(name, pos, count?. spread?, speed?, player?)` Renders a cloud of particles `name` centered around `pos` position, by default `count` 10 of them, default `speed` of 0, and to all players nearby, but these options can be changed via optional arguments. Follow vanilla `/particle` command on details on those options. Valid particle names are for example `'angry_villager', 'item diamond', 'block stone', 'dust 0.8 0.1 0.1 4'`. -### `particle_line(name, pos, pos2, density?)` +### `particle_line(name, pos, pos2, density?, player?)` Renders a line of particles from point `pos` to `pos2` with supplied density (defaults to 1), which indicates how far -apart you would want particles to appear, so `0.1` means one every 10cm. +apart you would want particles to appear, so `0.1` means one every 10cm. If a player (or player name) is supplied, only +that player will receive particles. -### `particle_rect(name, pos, pos2, density?)` -Renders a cuboid of particles between point `pos` to `pos2` with supplied density. +### `particle_box(name, pos, pos2, density?, player?)` +### `particle_rect` (deprecated) + +Renders a cuboid of particles between points `pos` and `pos2` with supplied density. If a player (or player name) is +supplied, only that player will receive particles. ## Markers +### `draw_shape(shape, duration, key?, value?, ... )`, +### `draw_shape(shape, duration, l(key?, value?, ... ))`, +### `draw_shape(shape, duration, attribute_map)` +### `draw_shape(shape_list)` + +Draws a shape in the world that will expire in `duration` ticks. Other attributes of the shape should be provided as +consecutive key - value argument pairs, either as next arguments, or packed in a list, or supplied as a proper key-value +`map`. Arguments may include shared shape attributes, which are all optional, as well as shape-specific attributes, that +could be either optional or required. Shapes will draw properly on all carpet clients. Other connected players that don't +have carpet installed will still be able to see the required shapes in the form of dust particles. Replacement shapes +are not required to follow all attributes precisely, but will allow vanilla clients to receive some experience of your +apps. One of the attributes that will definitely not be honored is the duration - particles will be send once +per shape and last whatever they typically last in the game. + +Shapes can be send one by one, using either of the first three invocations, or batched as a list of shape descriptors. +Batching has this benefit that they will be send possibly as one packet, limiting network overhead of +sending many small packets to draw several shapes at once. + +Shapes will fail to draw and raise a runtime error if not all its required parameters +are specified and all available shapes have some parameters that are required, so make sure to have them in place: + +On the client, shapes can recognize that they are being redrawn again with the same parameters, disregarding the +duration parameter. This updates the expiry on the drawn shape to the new value, instead of adding new shape in its +place. This can be used for toggling the shapes on and off that has been send previously with very large durations, +or simply refresh the shapes periodically in more dynamic applications. + +Optional shared shape attributes: + * `color` - integer value indicating the main color of the shape in the form of red, green, blue and alpha components + in the form of `0xRRGGBBAA`, with the default of `-1`, so white opaque, or `0xFFFFFFFF`. + * `player` - name or player entity to send the shape to. If specified, the shapes will appear only for the specified + player, otherwise it will be send to all players in the dimension. + * `width` - line thickness, defaults to 2.0pt + * `fill` - color for the faces, defaults to no fill. Use `color` attribute format + * `follow` - entity, or player name. Shape will follow an entity instead of being static. + Follow attribute requires all positional arguments to be relative to the entity and disallow + of using entity or block as position markers. You must specify positions as a triple. + * `snap` - if `follow` is present, indicated on which axis the snapping to entity coordinates occurs, and which axis + will be treated statically, i.e. the coordinate passed in a coord triple is the actual value in the world. Default + value is `'xyz'`, meaning the shape will be drawn relatively to the entity in all three directions. Using `xz` for + instance makes so that the shape follows the entity, but stays at the same, absolute Y coordinate. + +Available shapes: + * `'line'` - draws a straight line between two points + * Required attributes: + * `from` - triple coordinates, entity, or block value indicating one end of the line + * `to` - other end of the line, same format as `from` + + * `'box'` - draws a box with corners in specified points + * Required attributes: + * `from` - triple coordinates, entity, or block value indicating one corner of the box + * `to` - other corner, same format as `from` + + * `'sphere'`: + * Required attributes: + * `center` - center of the sphere + * `radius` - radius of the sphere + * Optional attributes: + * `level` - level of details, or grid size. The more the denser your sphere. Default level of 0, means that the + level of detail will be selected automatically based on radius. + + * `'cylinder'`: + * Required attributes: + * `center` - center of the base + * `radius` - radius of the base circle + * Optional attributes: + * `axis` - cylinder direction, one of `'x'`, `'y'`, `'z'` defaults to `'y'` + * `height` - height of the cyllinder, defaults to `0`, so flat disk. Can be negative. + * `level` - level of details, see `'sphere'`. + ### `create_marker(text, pos, rotation?, block?)` Spawns a (permanent) marker entity with text or block at position. Returns that entity for further manipulations. Unloading the app that spawned them will cause all the markers from the loaded portion of the world to be removed. -Also - if the game loads that marker in the future and the app is not loaded, it will be removed as well. +Also, if the game loads that marker in the future and the app is not loaded, it will be removed as well. ### `remove_all_markers()` @@ -2977,16 +3252,127 @@ as a string value. Displays the result of the expression to the chat. Overrides default `scarpet` behaviour of sending everyting to stderr. +### `format(components, ...)`, `format(l(components, ...))` + +Creates a line of formatted text. Each component is either a string indicating formatting and text it corresponds to +or a decorator affecting the component preceding it. + +Regular formatting components is a string that have the structure of: +`' '`, like `'gi Hi'`, which in this case indicates a grey, italicised word `'Hi'`. The space to separate the format and the text is mandatory. The format can be empty, but the space still +needs to be there otherwise the first word of the text will be used as format, which nobody wants. + +Format is a list of formatting symbols indicating the format. They can be mixed and matched although color will only be +applied once. Available symbols include: + * `i` - _italic_ + * `b` - **bold** + * `s` - ~~strikethrough~~ + * `u` - underline + * `o` - obfuscated + +And colors: + * `w` - White (default) + * `y` - Yellow + * `m` - Magenta (light purple) + * `r` - Red + * `c` - Cyan (aqua) + * `l` - Lime + * `t` - lighT blue + * `f` - dark grayF (weird Flex, but ok) + * `g` - Gray + * `d` - golD + * `p` - PurPle + * `n` - browN (dark red) + * `q` - turQuoise (dark aqua) + * `e` - grEEn + * `v` - naVy blue + * `k` - blaK + * `#FFAACC` - arbitrary RGB color (1.16+), hex notation. Use uppercase for A-F symbols + +Decorators (listed as extra argument after the component they would affect): + * `'^ '` - hover over tooltip text, appearing when hovering with your mouse over the text below. + * `'?` - command suggestion - a message that will be pasted to chat when text below it is clicked. + * `'!'` - a chat message that will be executed when the text below it is clicked. + +Both suggestions and messages can contain a command, which will be executed as a player that clicks it. + +So far the only usecase for formatted texts is with a `print` command. Otherwise it functions like a normal +string value representing what is actually displayed on screen. + +Example usages: +
+ print(format('rbu Error: ', 'r Stuff happened!'))
+ print(format('w Click ','tb [HERE]', '^di Awesome!', '!/kill', 'w \ button to win $1000'))
+  // the reason why I backslash the second space is that otherwise command parser may contract consecutive spaces
+  // not a problem in apps
+
+ ### `logger(expr)` Prints the message to system logs, and not to chat. +### read_file(resource, type) +### delete_file(resource, type) +### write_file(resource, type, data, ...) + +With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its + content, or deletes the resource. + +Resource is identified by a path to the file. +A path can contain letters, numbers and 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 +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 +resources is guaranteed to be isolated from other apps. Shared resources are... well, shared across all apes, meaning +they can eat of each others file, however all access to files is synchronized, and files are never left open, so +this should not lead to any access problems. + +If the app's name is `'foo'`, the script location would +be `world/scripts/foo.sc`, app +specific data directory is under `world/scripts/foo.data/...`, and shared data space is under +`world/scripts/shared/...`. + +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')) + +To log a single line of string + +Supported values for resource `type` is: + * `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 + +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. + +
+write_file('foo', 'shared_text, l('one', 'two'));
+write_file('foo', 'shared_text', 'three\n', 'four\n');
+write_file('foo', 'shared_raw', 'five\n', 'six\n');
+
+read_file('foo', 'shared_text')     => ['one', 'two', 'three', '', 'four', '', 'five', 'six']
+
+ ### `run(expr)` Runs a vanilla command from the string result of the `expr` and returns its success count
-run('fill 1 1 1 10 10 10 air') -> 123 // 123 block were filled, this operation was successful 123 times out of a possible 1000 blocks volume
+run('fill 1 1 1 10 10 10 air') -> 123 // 123 block were filled, this operation was successful 123 times out of a possible 1000 block volume
 run('give @s stone 4') -> 1 // this operation was successful once
 
@@ -2995,23 +3381,66 @@ run('give @s stone 4') -> 1 // this operation was successful once Performs autosave, saves all chunks, player data, etc. Useful for programs where autosave is disabled due to performance reasons and saves the world only on demand. -### `load_app_data(), load_app_data(file)` +### `load_app_data()` + +NOTE: usages with arguments, so `load_app_data(file)` and `load_app_data(file, shared?)` are deprecated. +Use `read_file` instead. Loads the app data associated with the app from the world /scripts folder. Without argument returns the memory -managed and buffered / throttled NBT tag. With a file name - reads explicitly a file with that name from the -scripts folder. +managed and buffered / throttled NBT tag. With a file name, reads explicitly a file with that name from the +scripts folder that belongs exclusively to the app. if `shared` is true, the file location is not exclusive +to the app anymore, but located in a shared app space. + +File descriptor can contain letters, numbers and folder separator: `'/'`. Any other characters are stripped +from the name before saving/loading. Empty descriptors are invalid. Do not add file extensions to the descriptor + +Function returns nbt value with the file content, or `null` if the file is missing or there were problems +with retrieving the data. + +The default no-name app, via `/script run` command can only save/load file from the shared data location. + +If the app's name is `'foo'`, the script location would +be `world/scripts/foo.sc`, system-managed default app data storage is in `world/scripts/foo.data.nbt`, app +specific data directory is under `world/scripts/foo.data/bar/../baz.nbt`, and shared data space is under +`world/scripts/shared/bar/../baz.nbt`. You can use app data to save non-vanilla information separately from the world and other scripts. -### `store_app_data(tag), store_app_data(tag, file)` +### `store_app_data(tag)` -Stores the app data associated with the app from the world /scripts folder. With the `file` parameter saves -immediately and with every call, without `file` parameter, it may take up to 10 seconds for the output file +Note: `store_app_data(tag, file)` and `store_app_data(tag, file, shared?)` usages deprecated. Use `write_file` instead. + +Stores the app data associated with the app from the world `/scripts` folder. With the `file` parameter saves +immediately and with every call to a specific file defined by the `file`, either in app space, or in the scripts +shared space if `shared` is true. Without `file` parameter, it may take up to 10 + seconds for the output file to sync preventing flickering in case this tag changes frequently. It will be synced when server closes. +Returns `true` if the file was saved successfully, `false` otherwise. + +Uses the same file structure for exclusive app data, and shared data folder as `load_app_data`. + ### `tick_time()` -Returns game tick counter. Can be used to run certain operations every n-th ticks, or to count in-game time +Returns server tick counter. Can be used to run certain operations every n-th ticks, or to count in-game time. + +### `world_time()` + +Returns dimension-specific tick counter. + +### `day_time(new_time?)` + +Returns current daytime clock value. If `new_time` is specified, sets a new clock +to that value. Daytime clocks are shared between all dimensions. + +### `last_tick_times()` + +Returns a 100-long array of recent tick times, in milliseconds. First item on the list is the most recent tick +If called outside of the main tick (either throgh scheduled tasks, or async execution), then the first item on the +list may refer to the previous tick performance. In this case the last entry (tick 100) would refer to the most current +tick. For all intent and purpose, `last_tick_times():0` should be used as last tick execution time, but +individual tick times may vary greatly, and these need to be taken with the little grain of +averaging. ### `game_tick(mstime?)` @@ -3037,6 +3466,9 @@ Returns current dimension that the script runs in. Evaluates the expression `expr` with different dimension execution context. `smth` can be an entity, world-localized block, so not `block('stone')`, or a string representing a dimension like: `'nether'`, `'the_nether'`, `'end'` or `'overworld'`, etc. + +### `view_distance()` +Returns the view distance of the server. ### `schedule(delay, function, args...)` @@ -3059,10 +3491,11 @@ Queries in-game statistics for certain values. Categories include: For the options of `entry`, consult your statistics page, or give it a guess. -The call will return `null` if the statistics options are incorrect, or player didn't get these in their history. +The call will return `null` if the statistics options are incorrect, or player doesn't have them in their history. If the player encountered the statistic, or game created for him empty one, it will return a number. Scarpet will not affect the entries of the statistics, even if it is just creating empty ones. With `null` response -it could either mean your input is wrong, or statistic has effectively a value of `0`.# `/script run` command +it could either mean your input is wrong, or statistic effectively has a value of `0`. +# `/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 diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md index 17b1d003..98021758 100644 --- a/docs/scarpet/api/Auxiliary.md +++ b/docs/scarpet/api/Auxiliary.md @@ -4,36 +4,112 @@ Collection of other methods that control smaller, yet still important aspects of ## Sounds -### `sound(name, pos, volume?, pitch?)` +### `sound(name, pos, volume?, pitch?, mixer?)` -Plays a specific sound `name`, at block or position `pos`, with optional `volume` and modified `pitch`. `pos` can be -either a block, triple of coords, or a list of thee numbers. Uses the same options as a corresponding `playsound` command. +Plays a specific sound `name`, at block or position `pos`, with optional `volume` and modified `pitch`, and under +optional `mixer`. Default values for `volume`, `pitch` and `mixer` are `1.0`, `1.0`, and `master`. +Valid mixer options are `master`, `music`, `record`, `weather`, `block`, `hostile`,`neutral`, `player`, `ambient` +and `voice`. `pos` can be either a block, triple of coords, or a list of thee numbers. Uses the same options as a + corresponding `playsound` command. ## Particles -### `particle(name, pos, count?. spread?, speed?, playername?)` +### `particle(name, pos, count?. spread?, speed?, player?)` Renders a cloud of particles `name` centered around `pos` position, by default `count` 10 of them, default `speed` of 0, and to all players nearby, but these options can be changed via optional arguments. Follow vanilla `/particle` command on details on those options. Valid particle names are for example `'angry_villager', 'item diamond', 'block stone', 'dust 0.8 0.1 0.1 4'`. -### `particle_line(name, pos, pos2, density?)` +### `particle_line(name, pos, pos2, density?, player?)` Renders a line of particles from point `pos` to `pos2` with supplied density (defaults to 1), which indicates how far -apart you would want particles to appear, so `0.1` means one every 10cm. +apart you would want particles to appear, so `0.1` means one every 10cm. If a player (or player name) is supplied, only +that player will receive particles. -### `particle_rect(name, pos, pos2, density?)` -Renders a cuboid of particles between point `pos` to `pos2` with supplied density. +### `particle_box(name, pos, pos2, density?, player?)` +### `particle_rect` (deprecated) + +Renders a cuboid of particles between points `pos` and `pos2` with supplied density. If a player (or player name) is +supplied, only that player will receive particles. ## Markers +### `draw_shape(shape, duration, key?, value?, ... )`, +### `draw_shape(shape, duration, l(key?, value?, ... ))`, +### `draw_shape(shape, duration, attribute_map)` +### `draw_shape(shape_list)` + +Draws a shape in the world that will expire in `duration` ticks. Other attributes of the shape should be provided as +consecutive key - value argument pairs, either as next arguments, or packed in a list, or supplied as a proper key-value +`map`. Arguments may include shared shape attributes, which are all optional, as well as shape-specific attributes, that +could be either optional or required. Shapes will draw properly on all carpet clients. Other connected players that don't +have carpet installed will still be able to see the required shapes in the form of dust particles. Replacement shapes +are not required to follow all attributes precisely, but will allow vanilla clients to receive some experience of your +apps. One of the attributes that will definitely not be honored is the duration - particles will be send once +per shape and last whatever they typically last in the game. + +Shapes can be send one by one, using either of the first three invocations, or batched as a list of shape descriptors. +Batching has this benefit that they will be send possibly as one packet, limiting network overhead of +sending many small packets to draw several shapes at once. + +Shapes will fail to draw and raise a runtime error if not all its required parameters +are specified and all available shapes have some parameters that are required, so make sure to have them in place: + +On the client, shapes can recognize that they are being redrawn again with the same parameters, disregarding the +duration parameter. This updates the expiry on the drawn shape to the new value, instead of adding new shape in its +place. This can be used for toggling the shapes on and off that has been send previously with very large durations, +or simply refresh the shapes periodically in more dynamic applications. + +Optional shared shape attributes: + * `color` - integer value indicating the main color of the shape in the form of red, green, blue and alpha components + in the form of `0xRRGGBBAA`, with the default of `-1`, so white opaque, or `0xFFFFFFFF`. + * `player` - name or player entity to send the shape to. If specified, the shapes will appear only for the specified + player, otherwise it will be send to all players in the dimension. + * `width` - line thickness, defaults to 2.0pt + * `fill` - color for the faces, defaults to no fill. Use `color` attribute format + * `follow` - entity, or player name. Shape will follow an entity instead of being static. + Follow attribute requires all positional arguments to be relative to the entity and disallow + of using entity or block as position markers. You must specify positions as a triple. + * `snap` - if `follow` is present, indicated on which axis the snapping to entity coordinates occurs, and which axis + will be treated statically, i.e. the coordinate passed in a coord triple is the actual value in the world. Default + value is `'xyz'`, meaning the shape will be drawn relatively to the entity in all three directions. Using `xz` for + instance makes so that the shape follows the entity, but stays at the same, absolute Y coordinate. + +Available shapes: + * `'line'` - draws a straight line between two points + * Required attributes: + * `from` - triple coordinates, entity, or block value indicating one end of the line + * `to` - other end of the line, same format as `from` + + * `'box'` - draws a box with corners in specified points + * Required attributes: + * `from` - triple coordinates, entity, or block value indicating one corner of the box + * `to` - other corner, same format as `from` + + * `'sphere'`: + * Required attributes: + * `center` - center of the sphere + * `radius` - radius of the sphere + * Optional attributes: + * `level` - level of details, or grid size. The more the denser your sphere. Default level of 0, means that the + level of detail will be selected automatically based on radius. + + * `'cylinder'`: + * Required attributes: + * `center` - center of the base + * `radius` - radius of the base circle + * Optional attributes: + * `axis` - cylinder direction, one of `'x'`, `'y'`, `'z'` defaults to `'y'` + * `height` - height of the cyllinder, defaults to `0`, so flat disk. Can be negative. + * `level` - level of details, see `'sphere'`. + ### `create_marker(text, pos, rotation?, block?)` Spawns a (permanent) marker entity with text or block at position. Returns that entity for further manipulations. Unloading the app that spawned them will cause all the markers from the loaded portion of the world to be removed. -Also - if the game loads that marker in the future and the app is not loaded, it will be removed as well. +Also, if the game loads that marker in the future and the app is not loaded, it will be removed as well. ### `remove_all_markers()` @@ -58,16 +134,127 @@ as a string value. Displays the result of the expression to the chat. Overrides default `scarpet` behaviour of sending everyting to stderr. +### `format(components, ...)`, `format(l(components, ...))` + +Creates a line of formatted text. Each component is either a string indicating formatting and text it corresponds to +or a decorator affecting the component preceding it. + +Regular formatting components is a string that have the structure of: +`' '`, like `'gi Hi'`, which in this case indicates a grey, italicised word `'Hi'`. The space to separate the format and the text is mandatory. The format can be empty, but the space still +needs to be there otherwise the first word of the text will be used as format, which nobody wants. + +Format is a list of formatting symbols indicating the format. They can be mixed and matched although color will only be +applied once. Available symbols include: + * `i` - _italic_ + * `b` - **bold** + * `s` - ~~strikethrough~~ + * `u` - underline + * `o` - obfuscated + +And colors: + * `w` - White (default) + * `y` - Yellow + * `m` - Magenta (light purple) + * `r` - Red + * `c` - Cyan (aqua) + * `l` - Lime + * `t` - lighT blue + * `f` - dark grayF (weird Flex, but ok) + * `g` - Gray + * `d` - golD + * `p` - PurPle + * `n` - browN (dark red) + * `q` - turQuoise (dark aqua) + * `e` - grEEn + * `v` - naVy blue + * `k` - blaK + * `#FFAACC` - arbitrary RGB color (1.16+), hex notation. Use uppercase for A-F symbols + +Decorators (listed as extra argument after the component they would affect): + * `'^ '` - hover over tooltip text, appearing when hovering with your mouse over the text below. + * `'?` - command suggestion - a message that will be pasted to chat when text below it is clicked. + * `'!'` - a chat message that will be executed when the text below it is clicked. + +Both suggestions and messages can contain a command, which will be executed as a player that clicks it. + +So far the only usecase for formatted texts is with a `print` command. Otherwise it functions like a normal +string value representing what is actually displayed on screen. + +Example usages: +
+ print(format('rbu Error: ', 'r Stuff happened!'))
+ print(format('w Click ','tb [HERE]', '^di Awesome!', '!/kill', 'w \ button to win $1000'))
+  // the reason why I backslash the second space is that otherwise command parser may contract consecutive spaces
+  // not a problem in apps
+
+ ### `logger(expr)` Prints the message to system logs, and not to chat. +### read_file(resource, type) +### delete_file(resource, type) +### write_file(resource, type, data, ...) + +With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its + content, or deletes the resource. + +Resource is identified by a path to the file. +A path can contain letters, numbers and 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 +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 +resources is guaranteed to be isolated from other apps. Shared resources are... well, shared across all apes, meaning +they can eat of each others file, however all access to files is synchronized, and files are never left open, so +this should not lead to any access problems. + +If the app's name is `'foo'`, the script location would +be `world/scripts/foo.sc`, app +specific data directory is under `world/scripts/foo.data/...`, and shared data space is under +`world/scripts/shared/...`. + +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')) + +To log a single line of string + +Supported values for resource `type` is: + * `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 + +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. + +
+write_file('foo', 'shared_text, l('one', 'two'));
+write_file('foo', 'shared_text', 'three\n', 'four\n');
+write_file('foo', 'shared_raw', 'five\n', 'six\n');
+
+read_file('foo', 'shared_text')     => ['one', 'two', 'three', '', 'four', '', 'five', 'six']
+
+ ### `run(expr)` Runs a vanilla command from the string result of the `expr` and returns its success count
-run('fill 1 1 1 10 10 10 air') -> 123 // 123 block were filled, this operation was successful 123 times out of a possible 1000 blocks volume
+run('fill 1 1 1 10 10 10 air') -> 123 // 123 block were filled, this operation was successful 123 times out of a possible 1000 block volume
 run('give @s stone 4') -> 1 // this operation was successful once
 
@@ -76,23 +263,66 @@ run('give @s stone 4') -> 1 // this operation was successful once Performs autosave, saves all chunks, player data, etc. Useful for programs where autosave is disabled due to performance reasons and saves the world only on demand. -### `load_app_data(), load_app_data(file)` +### `load_app_data()` + +NOTE: usages with arguments, so `load_app_data(file)` and `load_app_data(file, shared?)` are deprecated. +Use `read_file` instead. Loads the app data associated with the app from the world /scripts folder. Without argument returns the memory -managed and buffered / throttled NBT tag. With a file name - reads explicitly a file with that name from the -scripts folder. +managed and buffered / throttled NBT tag. With a file name, reads explicitly a file with that name from the +scripts folder that belongs exclusively to the app. if `shared` is true, the file location is not exclusive +to the app anymore, but located in a shared app space. + +File descriptor can contain letters, numbers and folder separator: `'/'`. Any other characters are stripped +from the name before saving/loading. Empty descriptors are invalid. Do not add file extensions to the descriptor + +Function returns nbt value with the file content, or `null` if the file is missing or there were problems +with retrieving the data. + +The default no-name app, via `/script run` command can only save/load file from the shared data location. + +If the app's name is `'foo'`, the script location would +be `world/scripts/foo.sc`, system-managed default app data storage is in `world/scripts/foo.data.nbt`, app +specific data directory is under `world/scripts/foo.data/bar/../baz.nbt`, and shared data space is under +`world/scripts/shared/bar/../baz.nbt`. You can use app data to save non-vanilla information separately from the world and other scripts. -### `store_app_data(tag), store_app_data(tag, file)` +### `store_app_data(tag)` -Stores the app data associated with the app from the world /scripts folder. With the `file` parameter saves -immediately and with every call, without `file` parameter, it may take up to 10 seconds for the output file +Note: `store_app_data(tag, file)` and `store_app_data(tag, file, shared?)` usages deprecated. Use `write_file` instead. + +Stores the app data associated with the app from the world `/scripts` folder. With the `file` parameter saves +immediately and with every call to a specific file defined by the `file`, either in app space, or in the scripts +shared space if `shared` is true. Without `file` parameter, it may take up to 10 + seconds for the output file to sync preventing flickering in case this tag changes frequently. It will be synced when server closes. +Returns `true` if the file was saved successfully, `false` otherwise. + +Uses the same file structure for exclusive app data, and shared data folder as `load_app_data`. + ### `tick_time()` -Returns game tick counter. Can be used to run certain operations every n-th ticks, or to count in-game time +Returns server tick counter. Can be used to run certain operations every n-th ticks, or to count in-game time. + +### `world_time()` + +Returns dimension-specific tick counter. + +### `day_time(new_time?)` + +Returns current daytime clock value. If `new_time` is specified, sets a new clock +to that value. Daytime clocks are shared between all dimensions. + +### `last_tick_times()` + +Returns a 100-long array of recent tick times, in milliseconds. First item on the list is the most recent tick +If called outside of the main tick (either throgh scheduled tasks, or async execution), then the first item on the +list may refer to the previous tick performance. In this case the last entry (tick 100) would refer to the most current +tick. For all intent and purpose, `last_tick_times():0` should be used as last tick execution time, but +individual tick times may vary greatly, and these need to be taken with the little grain of +averaging. ### `game_tick(mstime?)` @@ -118,6 +348,9 @@ Returns current dimension that the script runs in. Evaluates the expression `expr` with different dimension execution context. `smth` can be an entity, world-localized block, so not `block('stone')`, or a string representing a dimension like: `'nether'`, `'the_nether'`, `'end'` or `'overworld'`, etc. + +### `view_distance()` +Returns the view distance of the server. ### `schedule(delay, function, args...)` @@ -140,7 +373,7 @@ Queries in-game statistics for certain values. Categories include: For the options of `entry`, consult your statistics page, or give it a guess. -The call will return `null` if the statistics options are incorrect, or player didn't get these in their history. +The call will return `null` if the statistics options are incorrect, or player doesn't have them in their history. If the player encountered the statistic, or game created for him empty one, it will return a number. Scarpet will not affect the entries of the statistics, even if it is just creating empty ones. With `null` response -it could either mean your input is wrong, or statistic has effectively a value of `0`. \ No newline at end of file +it could either mean your input is wrong, or statistic effectively has a value of `0`. diff --git a/docs/scarpet/api/BlockIterations.md b/docs/scarpet/api/BlockIterations.md index 4f2085ff..f60375c8 100644 --- a/docs/scarpet/api/BlockIterations.md +++ b/docs/scarpet/api/BlockIterations.md @@ -2,11 +2,15 @@ These functions help scan larger areas of blocks without using generic loop functions, like nested `loop`. -### `scan(cx, cy, cz, dx, dy, dz, px?, py?, pz?, expr)` +### `scan(cx, cy, cz, dx, dy, dz, px?, py?, pz?, expr)`, `scan(center, range, lower_range?, expr)` -Evaluates expression over area of blocks defined by its center (`cx, cy, cz`), expanded in all directions -by `dx, dy, dz` blocks, or optionally in negative with `d` coords, and `p` coords in positive values. `expr` -receives `_x, _y, _z` as coords of current analyzed block and `_`, which represents the block itself. +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 +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, which would cause the expression not be evaluated for their boolean value. @@ -16,7 +20,7 @@ return value. `break` return value has no effect. ### `volume(x1, y1, z1, x2, y2, z2, expr)` -Evaluates expression for each block in the area, the same as the `scan`function, but using two opposite corners of +Evaluates expression for each block in the area, the same as the `scan` function, but using two opposite corners of the rectangular cuboid. Any corners can be specified, its like you would do with `/fill` command. For return value and handling `break` and `continue` statements, see `scan` function above. @@ -32,7 +36,7 @@ for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block ### `rect(cx, cy, cz, dx?, dy?, dz?, px?, py?, pz?)` returns an iterator, just like `range` function that iterates over rectangular cubarea of blocks. If only center -point is specified, it iterates over 27 blocks. If `d` arguments are specified, expands selection of respective +point is specified, it iterates over 27 blocks. If `d` arguments are specified, expands selection by the respective number of blocks in each direction. If `p` arguments are specified, it uses `d` for negative offset, and `p` for positive. ### `diamond(cx, cy, cz, radius?, height?)` diff --git a/docs/scarpet/api/BlocksAndWorldAccess.md b/docs/scarpet/api/BlocksAndWorldAccess.md index 7c31b2cd..522faff2 100644 --- a/docs/scarpet/api/BlocksAndWorldAccess.md +++ b/docs/scarpet/api/BlocksAndWorldAccess.md @@ -70,8 +70,8 @@ set(x,y,z,'hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count: Evaluates subexpression without causing updates when blocks change in the world -Consider following scenario: We would like to generate a bunch of terrain in a flat world following a perling noise -generator. The following code causes cascading effect as blocks placed on chunk borders will cause other chunks to get +Consider following scenario: We would like to generate a bunch of terrain in a flat world following a perlin noise +generator. The following code causes a cascading effect as blocks placed on chunk borders will cause other chunks to get loaded to full, thus generated:
@@ -85,7 +85,7 @@ __on_chunk_generated(x, z) -> (
 )
 
-The following addition resolves this issue, by not allowing block updates pass chunk borders: +The following addition resolves this issue, by not allowing block updates past chunk borders:
 __config() -> m(l('scope', 'global'));
@@ -102,19 +102,26 @@ __on_chunk_generated(x, z) -> (
 
 ### `place_item(item, pos, facing?, sneak?)`
 
-Places a given item in the world like it was placed by a player. Item names are default minecraft item name, 
+Uses a given item in the world like it was used by a player. Item names are default minecraft item name, 
 less the minecraft prefix. Default facing is 'up', but there are other options: 'down', 'north', 'east', 'south', 
 'west', but also there are other secondary directions important for placement of blocks like stairs, doors, etc. 
 Try experiment with options like 'north-up' which is placed facing north with cursor pointing to the upper part of the 
 block, or 'up-north', which means a block placed facing up (player looking down) and placed smidge away of the block 
 center towards north. Optional sneak is a boolean indicating if a player would be sneaking while placing the 
-block - this option only affects placement of chests and scaffolding at the moment. Returns true if placement was 
+block - this option only affects placement of chests and scaffolding at the moment. 
+
+Works with items that have the right-click effect on the block placed, like `bone_meal` on grass or axes on logs,
+but doesn't open chests / containers, so have no effect on interactive blocks, like TNT, comparators, etc.
+
+Returns true if placement/use was 
 successful, false otherwise.
 
 
 place_item('stone',x,y,z) // places a stone block on x,y,z block
 place_item('piston,x,y,z,'down') // places a piston facing down
 place_item('carrot',x,y,z) // attempts to plant a carrot plant. Returns true if could place carrots at that position.
+place_item('bone_meal',x,y,z) // attempts to bonemeal the ground.
+place_item('wooden_axe',x,y,z) // attempts to strip the log.
 
### `set_poi(pos, type, occupancy?)` @@ -127,9 +134,10 @@ If type is `null`, POI at position is removed. In any case, previous POI is also Interestingly, `unemployed`, and `nitwit` are not used in the game, meaning, they could be used as permanent spatial markers for scarpet apps. `meeting` is the only one with increased max occupancy of 32. -### `set_biome(pos, biome_name)` +### `set_biome(pos, biome_name, update=true)` -changes biome at that block position. +Changes the biome at that block position. if update is specified and false, then chunk will not be refreshed +on the clients. Biome changes can only be send to clients with the entire data from the chunk. ### `update(pos)` @@ -145,7 +153,7 @@ Causes a random tick at position. ### `destroy(pos), destroy(pos, -1), destroy(pos, ), destroy(pos, tool, nbt?)` -Destroys the block like it was mined by a player. Add -1 for silk touch, and positive number for fortune level. +Destroys the block like it was mined by a player. Add -1 for silk touch, and a positive number for fortune level. If tool is specified, and optionally its nbt, it will use that tool and will attempt to mine the block with this tool. If called without item context, this function, unlike harvest, will affect all kinds of blocks. If called with item in context, it will fail to break blocks that cannot be broken by a survival player. @@ -161,28 +169,28 @@ for mining. Obviously, in this case the use of `harvest` would be much more appl
 mine(x,y,z) ->
 (
-   p = player();
-   slot = p~'selected_slot';
-   item_tuple = inventory_get(p, slot);
-   if (!item_tuple, destroy(x,y,z,'air'); return()); // empty hand, just break with 'air'
-   l(item, count, tag) = item_tuple;
-   tag_back = destroy(x,y,z, item, tag);
-   if (tag_back == false, // failed to break the item
-	     return(tag_back)
-   );
-   if (tag_back == true, // block broke, tool has no tag
-	     return(tag_back)
-   );
-   if (tag_back == null, //item broke
-	     delete(tag:'Damage');
-	     inventory_set(p, slot, count-1, item, tag);
-	     return(tag_back)
-   );
-   if (type(tag_back) == 'nbt', // item didn't break, here is the effective nbt
-	     inventory_set(p, slot, count, item, tag_back);
-	     return(tag_back)
-   );
-   print('How did we get here?');
+  p = player();
+  slot = p~'selected_slot';
+  item_tuple = inventory_get(p, slot);
+  if (!item_tuple, destroy(x,y,z,'air'); return()); // empty hand, just break with 'air'
+  l(item, count, tag) = item_tuple;
+  tag_back = destroy(x,y,z, item, tag);
+  if (tag_back == false, // failed to break the item
+    return(tag_back)
+  );
+  if (tag_back == true, // block broke, tool has no tag
+    return(tag_back)
+  );
+  if (tag_back == null, //item broke
+    delete(tag:'Damage');
+    inventory_set(p, slot, count-1, item, tag);
+    return(tag_back)
+  );
+  if (type(tag_back) == 'nbt', // item didn't break, here is the effective nbt
+    inventory_set(p, slot, count, item, tag_back);
+    return(tag_back)
+  );
+  print('How did we get here?');
 )
 
@@ -200,9 +208,9 @@ Returns a triple of coordinates of a specified block or entity. Technically enti and the same can be achieved with `query(entity,'pos')`, but for simplicity `pos` allows to pass all positional objects.
-pos(block(0,5,0))  => l(0,5,0)
+pos(block(0,5,0)) => l(0,5,0)
 pos(players()) => l(12.3, 45.6, 32.05)
-pos(block('stone'))  => Error: Cannot fetch position of an unrealized block
+pos(block('stone')) => Error: Cannot fetch position of an unrealized block
 
### `pos_offset(pos, direction, amount?)` @@ -260,51 +268,51 @@ poi(x,y,z,5) => [['nether_portal',0,[7,8,9]],['nether_portal',0,[7,9,9]]] // two ### `biome(pos)` -returns biome at that block position. +Returns the biome at that block position. ### `solid(pos)` -Boolean function, true if the block is solid +Boolean function, true if the block is solid. ### `air(pos)` -Boolean function, true if a block is air.... or cave air... or void air.... or any other air they come up with. +Boolean function, true if a block is air... or cave air... or void air... or any other air they come up with. ### `liquid(pos)` -Boolean function, true if the block is liquid, or liquidlogged +Boolean function, true if the block is liquid, or waterlogged (with any liquid). ### `flammable(pos)` -Boolean function, true if the block is flammable +Boolean function, true if the block is flammable. ### `transparent(pos)` -Boolean function, true if the block is transparent +Boolean function, true if the block is transparent. ### `opacity(pos)` -Numeric, returning opacity level of a block +Numeric function, returning the opacity level of a block. ### `blocks_daylight(pos)` -Boolean function, true if the block blocks daylight +Boolean function, true if the block blocks daylight. ### `emitted_light(pos)` -Numeric, returning light level emitted from block +Numeric function, returning the light level emitted from the block. ### `light(pos)` -Integer function, returning total light level at position +Numeric function, returning the total light level at position. ### `block_light(pos)` -Integer function, returning block light at position. From torches and other light sources. +Numeric function, returning the block light at position (from torches and other light sources). ### `sky_light(pos)` -Numeric function, returning sky light at position. From the sky access. +Numeric function, returning the sky light at position (from sky access). ### `see_sky(pos)` @@ -352,7 +360,7 @@ Boolean function, true if the block ticks randomly. ### `blocks_movement(pos)` -Boolean function, true if block at position blocks movement. +Boolean function, true if the block at position blocks movement. ### `block_sound(pos)` @@ -478,11 +486,11 @@ Returns the map colour of a block at position. One of: ### `loaded(pos)` Boolean function, true if the block is accessible for the game mechanics. Normally `scarpet` doesn't check if operates -on loaded area - the game will automatically load missing blocks. We see this as advantage. Vanilla `fill/clone` +on loaded area - the game will automatically load missing blocks. We see this as an advantage. Vanilla `fill/clone` commands only check the specified corners for loadness. -To check if block is truly loaded, I mean in memory, use `generation_status(x) != null`, as chunks can still be loaded -outside of the playable area, just are not used any of the game mechanics processes. +To check if a block is truly loaded, I mean in memory, use `generation_status(x) != null`, as chunks can still be loaded +outside of the playable area, just are not used by any of the game mechanic processes.
 loaded(pos(players()))  => 1
@@ -497,7 +505,7 @@ Deprecated as of scarpet 1.6, use `loaded_status(x) > 0`, or just `loaded(x)` wi
 
 ### `loaded_status(pos)`
 
-Returns loaded status as per new 1.14 chunk ticket system, 0 for inaccessible, 1 for border chunk, 2 for ticking, 
+Returns loaded status as per new 1.14 chunk ticket system, 0 for inaccessible, 1 for border chunk, 2 for redstone ticking, 
 3 for entity ticking
 
 ### `is_chunk_generated(pos)`, `is_chunk_generated(pos, force)`
@@ -519,13 +527,41 @@ Returns generation status as per the ticket system. Can return any value from se
 can only be stable in a few states: `full`, `features`, `liquid_carvers`, and `structure_starts`. Returns `null` 
 if the chunk is not in memory unless called with optional `true`.
 
+### `inhabited_time(pos)`
+
+Returns inhabited time for a chunk.
+
+### `spawn_potential(pos)`
+
+Returns spawn potential at a location (1.16+ only)
+
+### `structure_eligibility(pos, ?structure, ?size_needed)`
+
+Checks wordgen eligibility for a structure in a given chunk. If no structure is given, or `null`, then it will check
+ for all structures. If bounding box of the structures is also requested, it will compute size of potential
+  structures. This function, unlike other in the `structure*` category is not using world data nor accesses chunks
+  making it preferred for scoping ungenerated terrain, but it takes some compute resources to calculate the structure.
+  
+  Unlike `'structure'` this will return a tentative structure location. Random factors in world generation may prevent
+  the actual structure from forming.
+  
+If structure is specified, it will return `null` if a chunk is not eligible, `true` if the structure should appear, or 
+a map with two values: `'box'` for a pair of coordinates indicating bounding box of the structure, and `'pieces'` for 
+list of elements of the structure (as a tuple), with its name, direction, and box coordinates of the piece.
+
+If structure is not specified, it will return a set of structure names that are eligible, or a map with structures
+as keys, and same type of map values as with a single structure call. An empty set or an empty map would indicate that nothing
+should be generated there.
+
+
 ### `structures(pos), structures(pos, structure_name)`
 
 Returns structure information for a given block position. Note that structure information is the same for all the 
 blocks from the same chunk. `structures` function can be called with a block, or a block and a structure name. In 
 the first case it returns a map of structures at a given position, keyed by structure name, with values indicating 
 the bounding box of the structure - a pair of two 3-value coords (see examples). When called with an extra structure 
-name, returns list of components for that structure, with their name, direction and two sets of coordinates 
+name, returns a map with two values, `'box'` for bounding box of the structure, and `'pieces'` for a list of 
+components for that structure, with their name, direction and two sets of coordinates 
 indicating the bounding box of the structure piece.
 
 ### `structure_references(pos), structure_references(pos, structure_name)`
@@ -540,7 +576,7 @@ then to get its bounding boxes.
 
 Creates or removes structure information of a structure associated with a chunk of `pos`. Unlike `plop`, blocks are 
 not placed in the world, only structure information is set. For the game this is a fully functional structure even 
-if blocks are not set. To remove structure a given point is in, use `structure_references` to find where current 
+if blocks are not set. To remove the structure a given point is in, use `structure_references` to find where current 
 structure starts.
 
 ### `plop(pos, what)`
@@ -586,6 +622,13 @@ Structure list:
 *   `village_savanna`: Savanna, acacia village.
 *   `village_taiga`: Taiga, spruce village.
 *   `village_snowy`: Resolute, Canada.
+*   `nether_fossil`: Pile of bones (1.16)
+*   `ruined_portal`: Ruined portal, random variant.
+*   `bastion_remnant`: Piglin bastion, random variant for the chunk (1.16)
+*   `bastion_remnant_housing`: Housing units version of a piglin bastion (1.16)
+*   `bastion_remnant_stable`: Hoglin stables version of q piglin bastion (1.16)
+*   `bastion_remnant_treasure`: Treasure room version of a piglin bastion (1.16)
+*   `bastion_remnant_bridge` : Bridge version of a piglin bastion (1.16)
 
 Feature list:
 
@@ -642,6 +685,18 @@ Feature list:
 *   `coral`: random coral structure. Require water to spawn.
 *   `sea_pickle`
 *   `boulder`: A rocky, mossy formation from a giant taiga biome. Doesn't update client properly, needs relogging.
+*   `crimson_fungus` (1.16)
+*   `warped_fungus` (1.16)
+*   `nether_sprouts` (1.16)
+*   `crimson_roots` (1.16)
+*   `warped_roots`  (1.16)
+*   `weeping_vines` (1.16)
+*   `twisting_vines` (1.16)
+*   `basalt_pillar` (1.16)
+
+### `reload_chunk(pos)`
+
+Sends full chunk data to clients. Useful when lots stuff happened and you want to refresh it on the clients.
 
 ### `reset_chunk(pos)`, `reset_chunk(from_pos, to_pos)`, `reset_chunk(l(pos, ...))`
 Removes and resets the chunk, all chunks in the specified area or all chunks in a list at once, removing all previous
@@ -657,3 +712,10 @@ It returns a `map` with a report indicating how many chunks were affected, and h
  * `layer_count_`: number of chunks for which a `` generation step has been performed
  * `layer_time_`: cumulative time for all chunks spent on generating `` step
  
+### add_chunk_ticket(pos, type, radius)
+
+Adds a chunk ticket at a position, which makes the game to keep the designated area centered around
+`pos` with radius of `radius` loaded for a predefined amount of ticks, defined by `type`. Allowed types
+are `portal`: 300 ticks, `teleport`: 5 ticks, and `unknown`: 1 tick. Radius can be from 1 to 32 ticks.
+
+This function is tentative - will likely change when chunk ticket API is properly fleshed out.
\ No newline at end of file
diff --git a/docs/scarpet/api/Entities.md b/docs/scarpet/api/Entities.md
index 5fec0d0b..a0524460 100644
--- a/docs/scarpet/api/Entities.md
+++ b/docs/scarpet/api/Entities.md
@@ -3,23 +3,23 @@
 ## Entity Selection
 
 Entities have to be fetched before using them. Entities can also change their state between calls to the script if 
-game happens either in between separate calls to the programs, or if the program calls `game_tick` on its own. 
-In this case - entities would need to be re-fetched, or the code should account for entities getting dead.
+game ticks occur either in between separate calls to the programs, or if the program calls `game_tick` on its own. 
+In this case - entities would need to be re-fetched, or the code should account for entities dying.
 
 ### `player(), player(type), player(name)`
 
 With no arguments, it returns the calling player or the player closest to the caller. Note that the main context 
-will receive `p` variable pointing to this player. With `type` or `name` specified it will try first to match a type, 
+will receive `p` variable pointing to this player. With `type` or `name` specified, it will try first to match a type, 
 returning a list of players matching a type, and if this fails, will assume its player name query retuning player with 
 that name, or `null` if no player was found. With `'all'`, list of all players in the game, in all dimensions, so end 
 user needs to be cautious, that you might be referring to wrong blocks and entities around the player in question. 
 With `type = '*'` it returns all players in caller dimension, `'survival'` returns all survival and adventure players,
 `'creative'` returns all creative players, `'spectating'` returns all spectating players, and `'!spectating'`, 
-all not-spectating players. If all fails, with `name`, the player in question, if is logged in.
+all not-spectating players. If all fails, with `name`, the player in question, if he/she is logged in.
 
 ### `entity_id(uuid), entity_id(id)`
 
-Fetching entities wither by their ID obtained via `entity ~ 'id'`, which is unique for a dimension and current world 
+Fetching entities either by their ID obtained via `entity ~ 'id'`, which is unique for a dimension and current world 
 run, or by UUID, obtained via `entity ~ 'uuid'`. It returns null if no such entity is found. Safer way to 'store' 
 entities between calls, as missing entities will be returning `null`. Both calls using UUID or numerical ID are `O(1)`, 
 but obviously using UUIDs takes more memory and compute.
@@ -38,12 +38,12 @@ selectors are available:
 ### `entity_area(type, cx, cy, cz, dx, dy, dz)`
 
 Returns entities of a specified type in an area centered on `cx, cy, cz` and at most `dx, dy, dz` blocks away from 
-the center point. Uses same selectors as `entities_list`
+the center point. Uses the same selectors as `entities_list`.
 
 ### `entity_selector(selector)`
 
-Returns entities satisfying given vanilla entity selector. Most complex among all the methods of selecting entities, 
-but the most capable. Selectors are cached so should be as fast as other methods of selecting entities.
+Returns entities satisifying given vanilla entity selector. Most complex among all the methods of selecting entities, 
+but the most capable. Selectors are cached so it should be as fast as other methods of selecting entities.
 
 ### `spawn(name, pos, nbt?)`
 
@@ -53,168 +53,186 @@ you get the entity back as a return value, which is swell.
 
 ## Entity Manipulation
 
-Unlike with blocks, that use plethora of vastly different querying functions, entities are queried with `query` 
-function and altered via `modify` function. Type of information needed or values to be modified are different for 
+Unlike with blocks, that use a plethora of vastly different querying functions, entities are queried with the `query` 
+function and altered via the `modify` function. Type of information needed or values to be modified are different for 
 each call.
 
 Using `~` (in) operator is an alias for `query`. Especially useful if a statement has no arguments, 
-which in this case can be radically simplified
+which in this case can be radically simplified:
 
 
 query(p, 'name') <=> p ~ 'name'     // much shorter and cleaner
 query(p, 'holds', 'offhand') <=> p ~ l('holds', 'offhand')    // not really but can be done
 
-### `query(e,'removed')` +### `query(e, 'removed')` -Boolean. True if the entity is removed +Boolean. True if the entity is removed. -### `query(e,'id')` +### `query(e, 'id')` Returns numerical id of the entity. Most efficient way to keep track of entites in a script. Ids are only unique within current game session (ids are not preserved between restarts), -and dimension (each dimension has its own ids which can overlap. +and dimension (each dimension has its own ids which can overlap). -### `query(e,'uuid')` +### `query(e, 'uuid')` -Returns UUID (unique id) of the entity. Can be used to access entities with the other vanilla commands and +Returns the UUID (unique id) of the entity. Can be used to access entities with the other vanilla commands and remains unique regardless of the dimension, and is preserved between game restarts. Apparently players cannot be -accessed via UUID, but name instead. +accessed via UUID, but should be accessed with their name instead.
 map(entities_area('*',x,y,z,30,30,30),run('kill '+query(_,'id'))) // doesn't kill the player
 
-### `query(e,'pos')` +### `query(e, 'pos')` -Triple of entity position +Triple of the entity's position -### `query(e,'location')` +### `query(e, 'location')` -Quin-tuple of entity position (x, y, and z coords), and rotation (yaw, pitch) +Quin-tuple of the entity's position (x, y, and z coords), and rotation (yaw, pitch) -### `query(e,'x'), query(e,'y'), query(e,'z')` +### `query(e, 'x'), query(e, 'y'), query(e, 'z')` -Respective entity coordinate +Respective component of entity's coordinates -### `query(e,'pitch'), query(e,'yaw')` +### `query(e, 'pitch'), query(e, 'yaw')` Pitch and Yaw or where entity is looking. -### `query(e,'look')` +### `query(e, 'look')` Returns a 3d vector where the entity is looking. -### `query(e,'motion')` +### `query(e, 'motion')` -Triple of entity motion vector, `l(motion_x, motion_y, motion_z)` +Triple of entity's motion vector, `l(motion_x, motion_y, motion_z)` -### `query(e,'motion_x'), query(e,'motion_y'), query(e,'motion_z')` +### `query(e, 'motion_x'), query(e, 'motion_y'), query(e, 'motion_z')` -Respective component of the motion vector +Respective component of the entity's motion vector -### `query(e,'name'), query(e,'custom_name'), query(e,'type')` +### `query(e, 'name'), query(e, 'display_name'), query(e, 'custom_name'), query(e, 'type')` String of entity name -
query(e,'name')  => Leatherworker
+
+query(e,'name')  => Leatherworker
 query(e,'custom_name')  => null
 query(e,'type')  => villager
 
-### `query(e,'is_riding')` +## `query(e, 'command_name')` -Boolean. True if riding another entity. +Returns a valid string to be used in commands to address an entity. Its UUID for all entities except +player, where its their name. -### `query(e,'is_ridden')` +
+run('/kill ' + e~'command_name');
+
-Boolean. True if another entity is riding it. +### `query(e, 'is_riding')` -### `query(e,'passengers')` +Boolean, true if the entity is riding another entity. + +### `query(e, 'is_ridden')` + +Boolean, true if another entity is riding it. + +### `query(e, 'passengers')` List of entities riding the entity. -### `query(e,'mount')` +### `query(e, 'mount')` Entity that `e` rides. -### `query(e,'tags')` +### `query(e, 'tags')` -List of entity tags. +List of entity's tags. -### `query(e,'has_tags',tag)` +### `query(e, 'has_tag',tag)` -Boolean, True if the entity is marked with `tag`. +Boolean, true if the entity is marked with `tag`. -### `query(e,'is_burning')` +### `query(e, 'is_burning')` -Boolean, True if the entity is burning. +Boolean, true if the entity is burning. -### `query(e,'fire')` +### `query(e, 'fire')` Number of remaining ticks of being on fire. -### `query(e,'silent')` +### `query(e, 'silent')` -Boolean, True if the entity is silent. +Boolean, true if the entity is silent. -### `query(e,'gravity')` +### `query(e, 'gravity')` -Boolean, True if the entity is affected by gravity, like most entities do. +Boolean, true if the entity is affected by gravity, like most entities are. -### `query(e,'immune_to_fire')` +### `query(e, 'immune_to_fire')` -Boolean, True if the entity is immune to fire. +Boolean, true if the entity is immune to fire. -### `query(e,'dimension')` +### `query(e, 'dimension')` -Name of the dimension entity is in. +Name of the dimension the entity is in. -### `query(e,'height')` +### `query(e, 'height')` -Height of the entity. +Height of the entity in blocks. -### `query(e,'width')` +### `query(e, 'width')` -Width of the entity. +Width of the entity in blocks. -### `query(e,'eye_height')` +### `query(e, 'eye_height')` -Eye height of the entity. +Eye height of the entity in blocks. -### `query(e,'age')` +### `query(e, 'age')` -Age, in ticks, of the entity, i.e. how long it existed. +Age of the entity in ticks, i.e. how long it existed. -### `query(e,'breeding_age')` +### `query(e, 'breeding_age')` -Breeding age of passive entity, in ticks. If negative it it time to adulthood, if positive, breeding cooldown +Breeding age of passive entity, in ticks. If negative, time to adulthood, if positive, breeding cooldown -### `query(e,'despawn_timer')` +### `query(e, 'despawn_timer')` -For living entities - the number of ticks they fall outside of immediate player presence. +For living entities, the number of ticks they fall outside of immediate player presence. -### `query(e,'item')` +### `query(e, 'portal_cooldown')` + +Number of ticks remaining until an entity can use a portal again. + +### `query(e, 'portal_timer')` + +Number of ticks an entity sits in a portal. + +### `query(e, 'item')` The item triple (name, count, nbt) if its an item entity, `null` otherwise -### `query(e,'count')` +### `query(e, 'count')` Number of items in a stack from item entity, `null` otherwise -### `query(e,'pickup_delay')` +### `query(e, 'pickup_delay')` Retrieves pickup delay timeout for an item entity, `null` otherwise -### `query(e,'is_baby')` +### `query(e, 'is_baby')` Boolean, true if its a baby. -### `query(e,'target')` +### `query(e, 'target')` Returns mob's attack target or null if none or not applicable. -### `query(e,'home')` +### `query(e, 'home')` Returns creature's home position or null if none or not applicable. @@ -229,31 +247,31 @@ Returns a pose of an entity, one of the following options * `'crouching'` * `'dying'` -### `query(e,'sneaking')` +### `query(e, 'sneaking')` -Boolean, true if entity is sneaking. +Boolean, true if the entity is sneaking. -### `query(e,'sprinting')` +### `query(e, 'sprinting')` -Boolean, true if entity is sprinting. +Boolean, true if the entity is sprinting. -### `query(e,'swimming')` +### `query(e, 'swimming')` -Boolean, true if entity is swimming. +Boolean, true if the entity is swimming. -### `query(e,'jumping')` +### `query(e, 'jumping')` -Boolean, true if entity is jumping. +Boolean, true if the entity is jumping. -### `query(e,'gamemode')` +### `query(e, 'gamemode')` String with gamemode, or `null` if not a player. -### `query(e,'gamemode_id')` +### `query(e, 'gamemode_id')` Good'ol gamemode id, or null if not a player. -### `query(e,'player_type')` +### `query(e, 'player_type')` Returns `null` if the argument is not a player, otherwise: @@ -265,18 +283,25 @@ Returns `null` if the argument is not a player, otherwise: * `shadow`: any carpet-shadowed real player * `realms`: ? -### `query(e,'ping')` +### `query(e, 'category')` +Returns a lowercase string containing the category of the entity (hostile, passive, water, ambient, misc) + +### `query(e, 'team')` + +Team name for entity, or `null` if no team is assigned. + +### `query(e, 'ping')` Player's ping in milliseconds, or `null` if its not a player. -### `query(e,'permission_level')` +### `query(e, 'permission_level')` Player's permission level, or `null` if not applicable for this entity. -### `query(e,'effect',name?)` +### `query(e, 'effect', name?)` Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short -effect name, amplifier, and remaining duration. With an argument, if the living entity has not that potion active, +effect name, amplifier, and remaining duration in ticks. With an argument, if the living entity has not that potion active, returns `null`, otherwise return a tuple of amplifier and remaining duration.
@@ -285,13 +310,20 @@ query(p,'effect','haste')  => [0, 177]
 query(p,'effect','resistance')  => null
 
-### `query(e,'health')` +### `query(e, 'health')` Number indicating remaining entity health, or `null` if not applicable. -### `query(e,'holds',slot?)` +### `query(e, 'hunger')` +### `query(e, 'saturation')` +### `query(e, 'exhaustion')` -Returns triple of short name, stack count, and NBT of item held in `slot`. Available options for `slot` are: +Retrieves player hunger related information. For non-players, returns `null`. + + +### `query(e, 'holds', slot?)` + +Returns triple of short name, stack count, and NBT of item held in `slot`, or `null` if nothing or not applicable. Available options for `slot` are: * `mainhand` * `offhand` @@ -302,16 +334,16 @@ Returns triple of short name, stack count, and NBT of item held in `slot`. Avail If `slot` is not specified, it defaults to the main hand. -### `query(e,'selected_slot')` +### `query(e, 'selected_slot')` -Number indicating the selected slot of entity inventory. Currently only applicable to players. +Number indicating the selected slot of entity's inventory. Currently only applicable to players. -### `query(e,'facing', order?)` +### `query(e, 'facing', order?)` Returns where the entity is facing. optional order (number from 0 to 5, and negative), indicating primary directions where entity is looking at. From most prominent (order 0) to opposite (order 5, or -1) -### `query(e,'trace', reach?, options?...)` +### `query(e, 'trace', reach?, options?...)` Returns the result of ray tracing from entity perspective, indicating what it is looking at. Default reach is 4.5 blocks (5 for creative players), and by default it traces for blocks and entities, identical to player attack tracing @@ -323,9 +355,9 @@ entities and blocks, blocks will take over the priority even if transparent or n Regardless of the options selected, the result could be `null` if nothing is in reach, entity, if look targets an entity, and block value if block is in reach. -### `query(e,'nbt',path?)` +### `query(e, 'nbt', path?)` -Returns full NBT of the entity. If path is specified, it fetches only that portion of the NBT, that corresponds to the +Returns full NBT of the entity. If path is specified, it fetches only the portion of the NBT that corresponds to the path. For specification of `path` attribute, consult vanilla `/data get entity` command. Note that calls to `nbt` are considerably more expensive comparing to other calls in Minecraft API, and should only @@ -337,13 +369,13 @@ type objects via `get, put, has, delete`, so try to use API calls first for that Like with entity querying, entity modifications happen through one function. Most position and movements modifications don't work currently on players as their position is controlled by clients. -Currently there is no ability to modify NBT directly, but you could always use `run('data modify entity` +Currently there is no ability to modify NBT directly, but you could always use `run('data modify entity ...')` -### `modify(e,'remove')` +### `modify(e, 'remove')` Removes (not kills) entity from the game. -### `modify(e,'kill')` +### `modify(e, 'kill')` Kills the entity. @@ -357,7 +389,7 @@ Changes full location vector all at once. ### `modify(e, 'x', x), modify(e, 'y', y), modify(e, 'z', z)` -Moves the entity in.... one direction. +Moves the entity in one direction. ### `modify(e, 'pitch', pitch), modify(e, 'yaw', yaw)` @@ -379,21 +411,35 @@ Sets the corresponding component of the motion vector. Adds a vector to the motion vector. Most realistic way to apply a force to an entity. -### `modify(e, 'custom_name'), modify(e, 'custom_name', name )` +### `modify(e, 'custom_name'), modify(e, 'custom_name', name)` + +Sets the custom name of the entity. ### `modify(e, 'age', number)` -Modifies entity's internal age counter. Fiddling with this will affect directly behaviours of complex +Modifies entity's internal age counter. Fiddling with this will affect directly AI behaviours of complex entities, so use it with caution. ### `modify(e, 'pickup_delay', number)` +Sets the pickup delay for the item entity. + ### `modify(e, 'breeding_age', number)` +Sets the breeding age for the animal. + ### `modify(e, 'despawn_timer', number)` Sets a custom despawn timer value. +### `modify(e, 'portal_cooldown', number)` + +Sets a custom number of ticks remaining until an entity can use a portal again. + +### `modify(e, 'portal_timer', number)` + +Sets a custom number of ticks an entity sits in a portal. + ### `modify(e, 'dismount')` Dismounts riding entity. @@ -412,11 +458,11 @@ Mounts on all listed entities on `e`. ### `modify(e, 'tag', tag, ? ...), modify(e, 'tag', l(tags) )` -Adds tag / tags to the entity. +Adds tag(s) to the entity. ### `modify(e, 'clear_tag', tag, ? ...), modify(e, 'clear_tag', l(tags) )` -Removes tag from entity. +Removes tag(s) from the entity. ### `modify(e, 'talk')` @@ -429,18 +475,63 @@ If called with `false` value, it will disable AI in the mob. `true` will enable ### `modify(e, 'no_clip', boolean)` Sets if the entity obeys any collisions, including collisions with the terrain and basic physics. Not affecting -players, since they are controlled client side +players, since they are controlled client side. -### `modify(e, 'effect', name, duration?, amplifier?, show_particles?, show_icon?)` +### `modify(e, 'effect', name?, duration?, amplifier?, show_particles?, show_icon?)` Applies status effect to the living entity. Takes several optional parameters, which default to `0`, `true` -and `true`. If no duration is specified, or it is null or 0, the effect is removed. +and `true`. If no duration is specified, or if it's null or 0, the effect is removed. If name is not specified, +it clears all effects. ### `modify(e, 'home', null), modify(e, 'home', block, distance?), modify(e, 'home', x, y, z, distance?)` Sets AI to stay around the home position, within `distance` blocks from it. `distance` defaults to 16 blocks. `null` removes it. _May_ not work fully with mobs that have this AI built in, like Villagers. +### `modify(e, 'gamemode', gamemode?), modify(e, 'gamemode', gamemode_id?)` + +Modifies gamemode of player to whatever string (case-insensitive) or number you put in. + +* 0: survival +* 1: creative +* 2: adventure +* 3: spectator + +### `modify(e, 'jumping', boolean)` + +Will make the entity constantly jump if set to true, and will stop the entity from jumping if set to false. +Note that jumping parameter can be fully controlled by the entity AI, so don't expect that this will have +a permanent effect. Use `'jump'` to make an entity jump once for sure. + +Requires a living entity as an argument. + +### `modify(e, 'jump'))` + +Will make the entity jump once. + +### `modify(e, 'silent', boolean)` + +Silences or unsilences the entity. + +### `modify(e, 'gravity', boolean)` + +Toggles gravity for the entity. + +### `modify(e, 'fire', ticks)` + +Will set entity on fire for `ticks` ticks. Set to 0 to extinguish. + +### `modify(e, 'hunger', value)` +### `modify(e, 'saturation', value)` +### `modify(e, 'exhaustion', value)` + +Modifies directly player raw hunger components. Has no effect on non-players + +### `modify(e, 'add_exhaustion', value)` + +adds exhaustion value to the current player exhaustion level - that's the method you probably want to use +to manipulate how much 'food' some action cost. + ## Entity Events There is a number of events that happen to entities that you can attach your own code to in the form of event handlers. @@ -451,19 +542,19 @@ defining the callback with `entity_event` The following events can be handled by entities. -* `'on_tick'`: executes every tick right before entity is ticked in the game. Required arguments: `entity` +* `'on_tick'`: executes every tick right before the entity is ticked in the game. Required arguments: `entity` * `'on_death'`: executes once when a living entity dies. Required arguments: `entity, reason` * `'on_removed'`: execute once when an entity is removed. Required arguments: `entity` * `'on_damaged'`: executed every time a living entity is about to receive damage. 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 will attach inapplicable event to it. +when you attach inapplicable event to it. ### `entity_event(e, event, call_name, args...)` -Attaches specific function from the current package to be called upon the `event`, with extra `args` curried to the -original required arguments for the event handler +Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the +original required arguments for the event handler.
 protect_villager(entity, amount, source, source_entity, healing_player) ->
diff --git a/docs/scarpet/api/Events.md b/docs/scarpet/api/Events.md
index aa9f1c98..6a616f30 100644
--- a/docs/scarpet/api/Events.md
+++ b/docs/scarpet/api/Events.md
@@ -19,7 +19,7 @@ __on_tick()         // can access blocks and entities in the overworld
 __on_tick_nether()  // can access blocks and entities in the nether
 __on_tick_ender()   // can access blocks and entities in the end
 __on_chunk_generated(x,z) // called after a chunk is promoted to the full chunk,
-                          // prodiving lowest x and z coords in the chunk
+                          // providing lowest x and z coords in the chunk
                           // event will not work with optifine installed in the game
 __on_lightning(block, mode) // mode is `true` if lightning caused horse trap to spawn
 // player specific callbacks
@@ -33,6 +33,11 @@ __on_player_interacts_with_block(player, hand, block, face, hitvec)  //right cli
 __on_player_places_block(player, item_tuple, hand, block) // player have just placed the block.
 __on_player_interacts_with_entity(player, entity, hand)
 __on_player_attacks_entity(player, entity)
+__on_player_takes_damage(player, amount, source, source_entity)
+__on_player_deals_damage(player, amount, entity)
+__on_player_dies(player)
+__on_player_respawns(player)
+__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension)
 __on_player_rides(player, forward, strafe, jumping, sneaking)
 __on_player_jumps(player)
 __on_player_deploys_elytra(player)
@@ -43,9 +48,16 @@ __on_player_starts_sprinting(player)
 __on_player_stops_sprinting(player)
 __on_player_drops_item(player)
 __on_player_drops_stack(player)
+__on_player_connects(player)
+__on_player_disconnects(player, reason)
 __on_statistic(player, category, event, value) // player statistic changes
 
+Couple special cases. +* `__on_player_changes_dimension` returns `null` as `to_pos` when player goes back to the overworld from the end +, since the respawn location of the player is not control by the teleport, or a player can see the end credits. After + the player is eligible to respawn in the overworld, `__on_player_respawns` will be triggered. + ### `/script event` command used to display current events and bounded functions. use `add_to` ro register new event, or `remove_from` to diff --git a/docs/scarpet/api/Inventories.md b/docs/scarpet/api/Inventories.md index 972cf5b5..5ef96c7a 100644 --- a/docs/scarpet/api/Inventories.md +++ b/docs/scarpet/api/Inventories.md @@ -4,7 +4,12 @@ Most functions in this category require inventory as the first argument. Inventory could be specified by an entity, or a block, or position (three coordinates) of a potential block with inventory. Player enderchest inventory require -two arguments, keyword `'enderchest'`, followed by the player entity argument. If the entity or a block doesn't have +two arguments, keyword `'enderchest'`, followed by the player entity argument, or a single argument as a string of a +form: `'enderchest_steve'`. If your player name starts with enderchest, it can be always accessed by passing a player +entity value. If all else fails, it will try to identify first three arguments as coordinates of a block position of +a block inventory. Player inventories can also be called by their name. + + If the entity or a block doesn't have an inventory, they 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. diff --git a/docs/scarpet/api/Scoreboard.md b/docs/scarpet/api/Scoreboard.md index a509bb15..b638fb93 100644 --- a/docs/scarpet/api/Scoreboard.md +++ b/docs/scarpet/api/Scoreboard.md @@ -17,9 +17,12 @@ scoreboard_add('counter') scoreboard_add('lvl','level')
-### `scoreboard_remove(objective)` +### `scoreboard_remove(objective)` `scoreboard_remove(objective, key)` -Removes an objective. Returns `true` if objective has existed and has been removed. +Removes an entire objective, or an entry in the scoreboard associated with the key. +Returns `true` if objective has existed and has been removed, or previous +value of the scoreboard if players score is removed. Returns `null` if objective didn't exist, or a key was missing +for the objective. ### `scoreboard_display(place, objective)` diff --git a/docs/scarpet/language/Overview.md b/docs/scarpet/language/Overview.md index 0689740f..0ad850f5 100644 --- a/docs/scarpet/language/Overview.md +++ b/docs/scarpet/language/Overview.md @@ -276,4 +276,4 @@ global_foo = floor(rand(10)); check_foo_not_zero(); ... check_foo_not_zero() -> if(global_foo == 0, global_foo = 1) -
\ No newline at end of file +
diff --git a/docs/scarpet/language/SystemFunctions.md b/docs/scarpet/language/SystemFunctions.md index e25b48ba..a1efa6d6 100644 --- a/docs/scarpet/language/SystemFunctions.md +++ b/docs/scarpet/language/SystemFunctions.md @@ -46,15 +46,22 @@ number('3bar') => null number('2')+number('2') => 4
-### `str(expr, params? ... ), str(expr, param_list)` +### `str(expr)`,`str(expr, params? ... )`, `str(expr, param_list)` -Returns a formatted string representing expression. Accepts formatting style accepted by `String.format`. -Supported types (with `"%?"` syntax): +If called with one argument, returns string representation of such value. + +Otherwise, returns a formatted string representing the expression. Arguments for formatting can either be provided as + each consecutive parameter, or as a list which then would be the only extra parameter. To format one list argument + , you can use `str(list)`, or `str('foo %s', l(list))`. + +Accepts formatting style accepted by `String.format`. +Supported types (with `"%"` syntax): * `d`, `o`, `x`: integers, octal, hex * `a`, `e`, `f`, `g`: floats * `b`: booleans * `s`: strings +* `%%`: '%' character
 str(null) => null
@@ -140,6 +147,18 @@ Returns true if task has completed, or false otherwise.
 
 Evaluates `expression` synchronized with respect to the lock `lock`. Returns the value of the expression.
 
+### `task_dock(expr)`
+
+In a not-task (running regular code on the main game thread) it is a pass-through command. In tasks - it docks
+the current thread on the main server thread and executes expression as one server offline server task.
+This is especially helpful in case a task has several docking operations to perform, such as setting a block, and
+it would be much more efficient to do them all at once rather then packing each block access in each own call.
+
+Be mindful, that docking the task means that the tick execution will be delayed until the expression is evaluated.
+This will synchronize your task with other tasks using `task_dock`, but if you should be using `synchronize` to
+synchronize tasks without locking the main thread.
+
+
 * * *
 
 ## Auxiliary functions
@@ -244,8 +263,11 @@ sleep(50)
 
 ### `time()`
 
-Returns the number of milliseconds since 'some point', like Java's `System.nanoTime()`. 
-It returns a float, which has 1 microsecond precision (0.001 ms)
+Returns the number of milliseconds since 'some point', like Java's `System.nanoTime()`, which varies from system to 
+system and from Java to Java. This measure should NOT be used to determine the current (date)time, but to measure
+durations of things.
+it returns a float with time in milliseconds (ms) for convenience and microsecond (μs) resolution for sanity.
+
 
 
 start_time = time();
diff --git a/docs/scarpet/resources/editors/idea/1.txt b/docs/scarpet/resources/editors/idea/1.txt
index fcab9dfb..8780d3f9 100644
--- a/docs/scarpet/resources/editors/idea/1.txt
+++ b/docs/scarpet/resources/editors/idea/1.txt
@@ -85,6 +85,7 @@ tanh
 task
 task_completed
 task_count
+task_dock
 task_join
 task_value
 throw
diff --git a/docs/scarpet/resources/editors/idea/2.txt b/docs/scarpet/resources/editors/idea/2.txt
index bbdf2f18..7e2a8d8a 100644
--- a/docs/scarpet/resources/editors/idea/2.txt
+++ b/docs/scarpet/resources/editors/idea/2.txt
@@ -1,4 +1,5 @@
 air
+add_chunk_ticket
 biome
 blast_resistance
 block
@@ -13,8 +14,10 @@ brightness
 chunk_tickets
 create_marker
 current_dimension
+day_time
 destroy
 diamond
+draw_shape
 drop_item
 emitted_light
 entity_area
@@ -24,18 +27,21 @@ entity_list
 entity_selector
 escape_nbt
 flammable
+format
 game_tick
 generation_status
 hardness
 harvest
 in_dimension
 in_slime_chunk
+inhabited_time
 inventory_find
 inventory_get
 inventory_remove
 inventory_set
 inventory_size
 item_category
+last_tick_times
 light
 liquid
 load_app_data
@@ -62,6 +68,7 @@ property
 query
 random_tick
 rect
+reload_chunk
 remove_all_markers
 reset_chunk
 run
@@ -82,10 +89,12 @@ sky_light
 solid
 sound
 spawn
+spawn_potential
 stack_limit
 statistic
 store_app_data
 structures
+structure_eligibility
 structure_references
 suffocates
 tick_time
@@ -93,5 +102,7 @@ ticks_randomly
 top
 transparent
 update
+view_distance
 volume
-without_updates
\ No newline at end of file
+without_updates
+world_time
\ No newline at end of file
diff --git a/docs/scarpet/resources/editors/idea/4.txt b/docs/scarpet/resources/editors/idea/4.txt
index 6c37a65a..faf87463 100644
--- a/docs/scarpet/resources/editors/idea/4.txt
+++ b/docs/scarpet/resources/editors/idea/4.txt
@@ -5,6 +5,7 @@ __on_close
 __on_player_attacks_entity
 __on_player_breaks_block
 __on_player_clicks_block
+__on_player_changes_dimension
 __on_player_deploys_elytra
 __on_player_drops_item
 __on_player_drops_stack
@@ -25,4 +26,10 @@ __on_player_wakes_up
 __on_statistic
 __on_tick
 __on_tick_ender
-__on_tick_nether
\ No newline at end of file
+__on_tick_nether
+__on_player_takes_damage
+__on_player_deals_damage
+__on_player_dies
+__on_player_respawns
+__on_player_disconnects
+__on_player_connects
\ No newline at end of file
diff --git a/docs/scarpet/resources/editors/npp/scarpet.xml b/docs/scarpet/resources/editors/npp/scarpet.xml
index 332d1030..33416a05 100644
--- a/docs/scarpet/resources/editors/npp/scarpet.xml
+++ b/docs/scarpet/resources/editors/npp/scarpet.xml
@@ -36,7 +36,7 @@
 
                 print return exit throw try
 
-                task task_count task_value task_join task_completed synchronize
+                task task_count task_value task_join task_completed synchronize task_dock
 
                 system_variable_get system_variable_set
 
@@ -54,8 +54,9 @@
                 hardness blast_resistance top loaded suffocates power ticks_randomly update block_tick random_tick in_slime_chunk
                 set without_updates
                 blocks_movement block_sound material map_colour property block_properties block_data poi set_poi
-                place_item set_biome biome loaded_status generation_status chunk_tickets
-                structures structure_references set_structure reset_chunk
+                place_item set_biome biome loaded_status generation_status inhabited_time spawn_potential chunk_tickets
+                structure_eligibility structures structure_references set_structure reset_chunk reload_chunk
+                add_chunk_ticket
 
                 scoreboard scoreboard_add scoreboard_remove scoreboard_display
 
@@ -65,11 +66,13 @@
 
                 scan volume neighbours rect diamond
 
-                logger sound particle particle_line particle_rect
+                format logger sound particle particle_line particle_rect
 
-                run save tick_time game_tick seed current_dimension in_dimension
+                run save tick_time world_time day_time last_tick_times game_tick seed current_dimension in_dimension view_distance
 
-                plop harvest destroy schedule nbt create_marker remove_all_markers load_app_data store_app_data escape_nbt statistic
+                plop harvest destroy schedule nbt draw_shape create_marker remove_all_markers
+
+                load_app_data store_app_data escape_nbt statistic
             
             euler pi null true false
             x y z p
@@ -83,6 +86,7 @@
                 __on_player_attacks_entity
                 __on_player_breaks_block
                 __on_player_clicks_block
+                __on_player_changes_dimension
                 __on_player_deploys_elytra
                 __on_player_drops_item
                 __on_player_drops_stack
@@ -104,6 +108,12 @@
                 __on_tick
                 __on_tick_ender
                 __on_tick_nether
+                __on_player_takes_damage
+                __on_player_deals_damage
+                __on_player_dies
+                __on_player_respawns
+                __on_player_disconnects
+                __on_player_connects
             
             $
             00' 01\ 02' 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
diff --git a/gradle.properties b/gradle.properties
index 03cd20b9..699591e3 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,11 +5,11 @@ org.gradle.jvmargs=-Xmx1G
 	# check these on https://fabricmc.net/use
     # or better: https://modmuss50.me/fabric.html
 	minecraft_version=1.15.2
-	yarn_mappings=1.15.2+build.14
-	loader_version=0.7.8+build.188
+	yarn_mappings=1.15.2+build.17
+	loader_version=0.8.7+build.201
 
 # Mod Properties
-	mod_version = 1.3.17+v200401
+	mod_version = 1.3.29+v200611
 	maven_group = carpet
 	archives_base_name = fabric-carpet
 
diff --git a/mergedoc.sh b/mergedoc.sh
new file mode 100755
index 00000000..16559437
--- /dev/null
+++ b/mergedoc.sh
@@ -0,0 +1,2 @@
+cat docs/scarpet/language/Overview.md docs/scarpet/language/VariablesAndConstants.md docs/scarpet/language/Operators.md docs/scarpet/language/Math.md docs/scarpet/language/SystemFunctions.md docs/scarpet/language/LoopsAndHigherOrderFunctions.md docs/scarpet/language/FunctionsAndControlFlow.md docs/scarpet/language/Containers.md docs/scarpet/api/Overview.md docs/scarpet/api/BlocksAndWorldAccess.md docs/scarpet/api/BlockIterations.md docs/scarpet/api/Entities.md docs/scarpet/api/Inventories.md docs/scarpet/api/Events.md docs/scarpet/api/Scoreboard.md docs/scarpet/api/Auxiliary.md docs/scarpet/api/ScriptCommand.md > docs/scarpet/Full.md
+
diff --git a/src/main/java/carpet/CarpetExtension.java b/src/main/java/carpet/CarpetExtension.java
index 7bb705cd..499c6c9e 100644
--- a/src/main/java/carpet/CarpetExtension.java
+++ b/src/main/java/carpet/CarpetExtension.java
@@ -6,6 +6,8 @@ import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.server.network.ServerPlayerEntity;
 
+import java.util.Map;
+
 public interface CarpetExtension
 {
     /**
@@ -51,4 +53,6 @@ public interface CarpetExtension
     default String version() {return null;}
 
     default void registerLoggers() {}
+
+    default Map canHasTranslations(String lang) { return null;}
 }
diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java
index 7f471737..85b6d31e 100644
--- a/src/main/java/carpet/CarpetServer.java
+++ b/src/main/java/carpet/CarpetServer.java
@@ -6,6 +6,7 @@ import java.util.List;
 import java.util.Random;
 
 import carpet.commands.*;
+import carpet.network.ServerNetworkHandler;
 import carpet.helpers.TickSpeed;
 import carpet.logging.LoggerRegistry;
 import carpet.script.CarpetScriptServer;
@@ -56,10 +57,11 @@ public class CarpetServer // static for now - easier to handle all around the co
             if (sm != null) sm.attachServer(server);
             e.onServerLoaded(server);
         });
-        scriptServer = new CarpetScriptServer();
+        scriptServer = new CarpetScriptServer(server);
         scriptServer.loadAllWorldScripts();
         MobAI.resetTrackers();
         LoggerRegistry.initLoggers();
+        //TickSpeed.reset();
     }
 
     public static void tick(MinecraftServer server)
@@ -102,29 +104,41 @@ public class CarpetServer // static for now - easier to handle all around the co
 
     public static void onPlayerLoggedIn(ServerPlayerEntity player)
     {
+        ServerNetworkHandler.onPlayerJoin(player);
         LoggerRegistry.playerConnected(player);
         extensions.forEach(e -> e.onPlayerLoggedIn(player));
+
     }
 
     public static void onPlayerLoggedOut(ServerPlayerEntity player)
     {
+        ServerNetworkHandler.onPlayerLoggedOut(player);
         LoggerRegistry.playerDisconnected(player);
         extensions.forEach(e -> e.onPlayerLoggedOut(player));
     }
 
     public static void onServerClosed(MinecraftServer server)
     {
+        ServerNetworkHandler.close();
         currentCommandDispatcher = null;
         if (scriptServer != null) scriptServer.onClose();
-        settingsManager.detachServer();
+
         LoggerRegistry.stopLoggers();
         extensions.forEach(e -> e.onServerClosed(server));
         minecraft_server = null;
+        disconnect();
     }
 
     public static void registerExtensionLoggers()
     {
         extensions.forEach(CarpetExtension::registerLoggers);
     }
+
+    public static void disconnect()
+    {
+        // this for whatever reason gets called multiple times even when joining;
+        TickSpeed.reset();
+        settingsManager.detachServer();
+    }
 }
 
diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java
index 79e32445..c93402ce 100644
--- a/src/main/java/carpet/CarpetSettings.java
+++ b/src/main/java/carpet/CarpetSettings.java
@@ -4,14 +4,13 @@ import carpet.settings.ParsedRule;
 import carpet.settings.Rule;
 import carpet.settings.SettingsManager;
 import carpet.settings.Validator;
+import carpet.utils.Translations;
 import carpet.utils.Messenger;
+import carpet.utils.SpawnChunks;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.server.dedicated.DedicatedServer;
-import net.minecraft.server.world.ChunkTicketType;
-import net.minecraft.server.world.ServerChunkManager;
 import net.minecraft.server.world.ServerWorld;
-import net.minecraft.util.Unit;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Box;
 import net.minecraft.util.math.ChunkPos;
@@ -35,7 +34,7 @@ import static carpet.settings.RuleCategory.CLIENT;
 @SuppressWarnings("CanBeFinal")
 public class CarpetSettings
 {
-    public static final String carpetVersion = "1.3.17+v200401";
+    public static final String carpetVersion = "1.3.29+v200611";
     public static final Logger LOG = LogManager.getLogger();
     public static boolean skipGenerationChecks = false;
     public static boolean impendingFillSkipUpdates = false;
@@ -43,6 +42,31 @@ public class CarpetSettings
     public static Vec3d fixedPosition = null;
     public static int runPermissionLevel = 2;
 
+    private static class LanguageValidator extends Validator {
+        @Override public String validate(ServerCommandSource source, ParsedRule currentRule, String newValue, String string) {
+            if (currentRule.get().equals(newValue) || source == null)
+            {
+                return newValue;
+            }
+            if (!Translations.isValidLanguage(newValue))
+            {
+                Messenger.m(source, "r "+newValue+" is not a valid language");
+                return null;
+            }
+            CarpetSettings.language = newValue;
+            Translations.updateLanguage(source);
+            return newValue;
+        }
+    }
+    @Rule(
+            desc = "sets the language for carpet",
+            category = FEATURE,
+            options = {"none", "zh_cn", "zh_tw"},
+            strict = false,
+            validate = LanguageValidator.class
+    )
+    public static String language = "none";
+
     @Rule(
             desc = "Nether portals correctly place entities going through",
             extra = "Entities shouldn't suffocate in obsidian",
@@ -71,6 +95,7 @@ public class CarpetSettings
     )
     public static int portalSurvivalDelay = 80;
 
+
     private static class OneHourMaxDelayLimit extends Validator {
         @Override public Integer validate(ServerCommandSource source, ParsedRule currentRule, Integer newValue, String string) {
             return (newValue > 0 && newValue <= 72000) ? newValue : null;
@@ -236,13 +261,6 @@ public class CarpetSettings
     @Rule( desc = "Pistons can push block entities, like hoppers, chests etc.", category = {EXPERIMENTAL, FEATURE} )
     public static boolean movableBlockEntities = false;
 
-    /*@Rule(
-            desc = "Orange stained glass behaves like slime, but doesn't stick to it",
-            extra = "Only available for 1.14. in 1.15 use honey instead",
-            category = {EXPERIMENTAL, FEATURE}
-    )
-    public static boolean honeySlime = false;*/
-
     @Rule( desc = "Saplings turn into dead shrubs in hot climates and no water access", category = FEATURE )
     public static boolean desertShrubs = false;
 
@@ -308,7 +326,7 @@ public class CarpetSettings
     )
     public static String commandPerimeterInfo = "true";
 
-    @Rule(desc = "Enables /draw commands", extra = "... allows for drawing simple shapes", category = COMMAND)
+    @Rule(desc = "Enables /draw commands", extra = {"... allows for drawing simple shapes or","other shapes which are sorta difficult to do normally"}, category = COMMAND)
     public static String commandDraw = "true";
 
     @Rule(
@@ -356,9 +374,6 @@ public class CarpetSettings
     @Rule(desc = "Pistons, Glass and Sponge can be broken faster with their appropriate tools", category = SURVIVAL)
     public static boolean missingTools = false;
 
-    //@Rule(desc = "Alternative, persistent caching strategy for nether portals", category = {SURVIVAL, CREATIVE})
-    //public static boolean portalCaching = false; // ineffective in 1.15
-
     @Rule(desc = "fill/clone/setblock and structure blocks cause block updates", category = CREATIVE)
     public static boolean fillUpdates = true;
 
@@ -472,7 +487,7 @@ public class CarpetSettings
     {
         @Override public Integer validate(ServerCommandSource source, ParsedRule currentRule, Integer newValue, String string)
         {
-            if (currentRule.get().equals(newValue))
+            if (currentRule.get().equals(newValue) || source == null)
             {
                 return newValue;
             }
@@ -486,8 +501,8 @@ public class CarpetSettings
             if (server.isDedicated())
             {
                 int vd = (newValue >= 2)?newValue:((DedicatedServer) server).getProperties().viewDistance;
-                if (vd != CarpetServer.minecraft_server.getPlayerManager().getViewDistance())
-                    CarpetServer.minecraft_server.getPlayerManager().setViewDistance(vd);
+                if (vd != server.getPlayerManager().getViewDistance())
+                    server.getPlayerManager().setViewDistance(vd);
                 return newValue;
             }
             else
@@ -509,13 +524,9 @@ public class CarpetSettings
     )
     public static int viewDistance = 0;
 
-    private static class DisableSpawnChunksValidator extends Validator {
-        @Override public Boolean validate(ServerCommandSource source, ParsedRule currentRule, Boolean newValue, String string) {
-            if (currentRule.get().booleanValue() == newValue.booleanValue())
-            {
-                //must been some startup thing
-                return newValue;
-            }
+    public static class ChangeSpawnChunksValidator extends Validator {
+        public static void changeSpawnSize(int size)
+        {
             ServerWorld overworld = CarpetServer.minecraft_server.getWorld(DimensionType.OVERWORLD);
             if (overworld != null) {
                 ChunkPos centerChunk = new ChunkPos(new BlockPos(
@@ -523,40 +534,39 @@ public class CarpetSettings
                         overworld.getLevelProperties().getSpawnY(),
                         overworld.getLevelProperties().getSpawnZ()
                 ));
-                ServerChunkManager chunkManager = (ServerChunkManager) overworld.getChunkManager();
-
-                chunkManager.removeTicket(ChunkTicketType.START, centerChunk, 11, Unit.INSTANCE);
-                if (!newValue)
-                {
-                    chunkManager.addTicket(ChunkTicketType.START, centerChunk, 11, Unit.INSTANCE);
-                }
+                SpawnChunks.changeSpawnChunks(overworld.getChunkManager(), centerChunk, size);
+            }
+        }
+        @Override public Integer validate(ServerCommandSource source, ParsedRule currentRule, Integer newValue, String string) {
+            if (source == null) return newValue;
+            if (newValue < 0 || newValue > 32)
+            {
+                Messenger.m(source, "r spawn chunk size has to be between 0 and 32");
+                return null;
+            }
+            if (currentRule.get().intValue() == newValue.intValue())
+            {
+                //must been some startup thing
+                return newValue;
+            }
+            if (CarpetServer.minecraft_server == null) return newValue;
+            ServerWorld currentOverworld = CarpetServer.minecraft_server.getWorld(DimensionType.OVERWORLD);
+            if (currentOverworld != null)
+            {
+                changeSpawnSize(newValue);
             }
             return newValue;
         }
     }
     @Rule(
-            desc = "Allows spawn chunks to unload",
+            desc = "Changes size of spawn chunks",
+            extra = {"Defines new radius", "setting it to 0 - disables spawn chunks"},
             category = CREATIVE,
-            validate = DisableSpawnChunksValidator.class
-    )
-    public static boolean disableSpawnChunks = false;
-
-    /*private static class KelpLimit extends Validator
-    {
-        @Override public Integer validate(ServerCommandSource source, ParsedRule currentRule, Integer newValue, String string) {
-            return (newValue>=0 && newValue <=25)?newValue:null;
-        }
-        @Override
-        public String description() { return "You must choose a value from 0 to 25. 25 and all natural kelp can grow 25 blocks, choose 0 to make all generated kelp not to grow";}
-    }
-    @Rule(
-            desc = "limits growth limit of newly naturally generated kelp to this amount of blocks",
-            options = {"0", "2", "25"},
-            category = FEATURE,
             strict = false,
-            validate = KelpLimit.class
+            options = {"0", "11"},
+            validate = ChangeSpawnChunksValidator.class
     )
-    public static int kelpGenerationGrowthLimit = 25;*/
+    public static int spawnChunksSize = 11;
 
     @Rule(desc = "Coral structures will grow with bonemeal from coral plants", category = FEATURE)
     public static boolean renewableCoral = false;
@@ -573,13 +583,6 @@ public class CarpetSettings
 
     @Rule(desc = "Spawning requires much less CPU and Memory", category = OPTIMIZATION)
     public static boolean lagFreeSpawning = false;
-
-    //@Rule(
-    //        desc = "Prevents horses and other mobs to wander into the distance after dismounting",
-    //        extra = "Fixes issues with various Joergens wandering off and disappearing client-side",
-    //        category = BUGFIX
-    //)
-    //public static boolean horseWanderingFix = false;
     
     @Rule(
             desc = "Allows structure mobs to spawn in flat worlds",
@@ -599,4 +602,54 @@ public class CarpetSettings
             category = CREATIVE
     )
     public static boolean extremeBehaviours = false;
+
+    @Rule(
+            desc = "Removes fog from client in the nether and the end",
+            extra = "Improves visibility, but looks weird",
+            category = CLIENT
+    )
+    public static boolean fogOff = false;
+
+    @Rule(
+            desc = "Creative No Clip",
+            extra = {
+                    "On servers it needs to be set on both ",
+                    "client and server to function properly.",
+                    "Has no effect when set on the server only",
+                    "Can allow to phase through walls",
+                    "if only set on the carpet client side",
+                    "but requires some trapdoor magic to",
+                    "allow the player to enter blocks"
+            },
+            category = {CREATIVE, CLIENT}
+    )
+    public static boolean creativeNoClip = false;
+
+
+    @Rule(
+            desc = "Creative flying speed multiplier",
+            extra = {
+                    "Purely client side setting, meaning that",
+                    "having it set on the decicated server has no effect",
+                    "but this also means it will work on vanilla servers as well"
+            },
+            category = {CREATIVE, CLIENT},
+            validate = Validator.NONNEGATIVE_NUMBER.class
+    )
+    public static double creativeFlySpeed = 1.0;
+
+    @Rule(
+            desc = "Creative air drag",
+            extra = {
+                    "Increased drag will slow down your flight",
+                    "So need to adjust speed accordingly",
+                    "With 1.0 drag, using speed of 11 seems to matching vanilla speeds.",
+                    "Purely client side setting, meaning that",
+                    "having it set on the decicated server has no effect",
+                    "but this also means it will work on vanilla servers as well"
+            },
+            category = {CREATIVE, CLIENT},
+            validate = Validator.PROBABILITY.class
+    )
+    public static double creativeFlyDrag = 0.09;
 }
diff --git a/src/main/java/carpet/commands/DrawCommand.java b/src/main/java/carpet/commands/DrawCommand.java
index 41fbcda6..f52e16d7 100644
--- a/src/main/java/carpet/commands/DrawCommand.java
+++ b/src/main/java/carpet/commands/DrawCommand.java
@@ -7,6 +7,11 @@ import com.google.common.collect.Lists;
 import com.mojang.brigadier.CommandDispatcher;
 import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
 import net.minecraft.block.Block;
 import net.minecraft.block.pattern.CachedBlockPosition;
 import net.minecraft.server.command.ServerCommandSource;
@@ -18,12 +23,15 @@ import net.minecraft.inventory.Inventory;
 import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.math.MathHelper;
 
 import java.util.List;
 import java.util.function.Predicate;
+import java.lang.Math;
 
 import static net.minecraft.server.command.CommandManager.argument;
 import static net.minecraft.server.command.CommandManager.literal;
+import static net.minecraft.server.command.CommandSource.suggestMatching;
 
 public class DrawCommand
 {
@@ -32,45 +40,142 @@ public class DrawCommand
         LiteralArgumentBuilder command = literal("draw").
                 requires((player) -> SettingsManager.canUseCommand(player, CarpetSettings.commandDraw)).
                 then(literal("sphere").
-                        then(argument("center",BlockPosArgumentType.blockPos()).
-                                then(argument("radius",IntegerArgumentType.integer(1)).
-                                        then(argument("block",BlockStateArgumentType.blockState()).
-                                                executes((c)-> drawCircle(
-                                                        c.getSource(),
-                                                        BlockPosArgumentType.getBlockPos(c, "center"),
-                                                        IntegerArgumentType.getInteger(c, "radius"),
-                                                        BlockStateArgumentType.getBlockState (c,"block"),
-                                                        null
-                                                        )
-                                                ).
-                                                then(literal("replace").
-                                                        then(argument("filter",BlockPredicateArgumentType.blockPredicate())
-                                                                .executes((c)-> drawCircle(
-                                                                        c.getSource(),
-                                                                        BlockPosArgumentType.getBlockPos(c, "center"),
-                                                                        IntegerArgumentType.getInteger(c, "radius"),
-                                                                        BlockStateArgumentType.getBlockState(c,"block"),
-                                                                        BlockPredicateArgumentType.getBlockPredicate(c,
-                                                                                "filter")
-                                                                        ))))))));
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(drawShape(c -> DrawCommand.drawSphere(c, false)))))).
+                then(literal("ball").
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(drawShape(c -> DrawCommand.drawSphere(c, true)))))).
+                then(literal("diamond").
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(drawShape(c -> DrawCommand.drawDiamond(c, true)))))).
+                then(literal("pyramid").
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(argument("height",IntegerArgumentType.integer(1)).
+                                                then(argument("pointing",StringArgumentType.word()).suggests( (c, b) -> suggestMatching(new String[]{"up","down"},b)).
+                                                        then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggestMatching(new String[]{"y","x","z"},b)).
+                                                                then(drawShape(c -> DrawCommand.drawPyramid(c, "square", true))))))))).
+                then(literal("cone").
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(argument("height",IntegerArgumentType.integer(1)).
+                                                then(argument("pointing",StringArgumentType.word()).suggests( (c, b) -> suggestMatching(new String[]{"up","down"},b)).
+                                                        then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggestMatching(new String[]{"y","x","z"},b))
+                                                                .then(drawShape(c -> DrawCommand.drawPyramid(c, "circle", true))))))))).
+                then(literal("cylinder").
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(argument("height",IntegerArgumentType.integer(1)).
+                                                        then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggestMatching(new String[]{"y","x","z"},b))
+                                                                .then(drawShape(c -> DrawCommand.drawPrism(c, "circle")))))))).
+                then(literal("cuboid").
+                        then(argument("center", BlockPosArgumentType.blockPos()).
+                                then(argument("radius", IntegerArgumentType.integer(1)).
+                                        then(argument("height",IntegerArgumentType.integer(1)).
+                                                then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggestMatching(new String[]{"y","x","z"},b))
+                                                        .then(drawShape(c -> DrawCommand.drawPrism(c, "square"))))))));
         dispatcher.register(command);
     }
 
-
-    private static int drawCircle(ServerCommandSource source, BlockPos pos, int radius, BlockStateArgument block,
-                                  Predicate replacement)
+    @FunctionalInterface
+    private interface OptionalBlockSelector
     {
-        return drawCircle(source, pos, (double)radius, (double)radius, (double)radius, block, replacement, false);
+        Integer apply(final CommandContext ctx) throws CommandSyntaxException;
     }
-    private static int drawCircle(ServerCommandSource source, BlockPos pos, double radiusX, double radiusY, double radiusZ,
-                                  BlockStateArgument block, Predicate replacement, boolean solid)
-    {
-        int affected = 0;
-        ServerWorld world = source.getWorld();
 
-        radiusX += 0.5;
-        radiusY += 0.5;
-        radiusZ += 0.5;
+    @FunctionalInterface
+    private interface ArgumentExtractor
+    {
+        T apply(final CommandContext ctx, final String argName) throws CommandSyntaxException;
+    }
+
+    private static RequiredArgumentBuilder
+    drawShape(OptionalBlockSelector drawer)
+    {
+        return argument("block", BlockStateArgumentType.blockState()).
+                executes(drawer::apply)
+                .then(literal("replace")
+                        .then(argument("filter", BlockPredicateArgumentType.blockPredicate())
+                                .executes(drawer::apply)));
+    }
+
+    private static class ErrorHandled extends RuntimeException {}
+
+    private static  T getArg(CommandContext ctx, ArgumentExtractor extract, String hwat) throws CommandSyntaxException
+    {
+        return getArg(ctx, extract, hwat, false);
+    }
+
+    private static  T getArg(CommandContext ctx, ArgumentExtractor extract, String hwat, boolean optional) throws CommandSyntaxException
+    {
+        T arg = null;
+        try
+        {
+            arg = extract.apply(ctx, hwat);
+        }
+        catch (IllegalArgumentException e)
+        {
+            if (optional) return null;
+            Messenger.m(ctx.getSource(), "rb Missing "+hwat);
+            throw new ErrorHandled();
+        }
+        return arg;
+    }
+
+    private static double lengthSq(double x, double y, double z)
+    {
+        return (x * x) + (y * y) + (z * z);
+    }
+
+    private static int setBlock(
+            ServerWorld world, BlockPos.Mutable mbpos, int x, int y, int z,
+            BlockStateArgument block, Predicate replacement,
+            List list
+    )
+    {
+        mbpos.set(x, y, z);
+        int success=0;
+        if (replacement == null || replacement.test(new CachedBlockPosition(world, mbpos, true)))
+        {
+            BlockEntity tileentity = world.getBlockEntity(mbpos);
+            if (tileentity instanceof Inventory)
+            {
+                ((Inventory) tileentity).clear();
+            }
+            if (block.setBlockState(world, mbpos, 2))
+            {
+                list.add(mbpos.toImmutable());
+                ++success;
+            }
+        }
+
+        return success;
+    }
+
+    private static int drawSphere(CommandContext ctx, boolean solid) throws CommandSyntaxException
+    {
+        BlockPos pos;
+        int radius;
+        BlockStateArgument block;
+        Predicate replacement;
+        try
+        {
+            pos = getArg(ctx, BlockPosArgumentType::getBlockPos, "center");
+            radius = getArg(ctx, IntegerArgumentType::getInteger, "radius");
+            block = getArg(ctx, BlockStateArgumentType::getBlockState, "block");
+            replacement = getArg(ctx, BlockPredicateArgumentType::getBlockPredicate, "filter", true);
+        }
+        catch (ErrorHandled ignored) { return 0; }
+
+        int affected = 0;
+        ServerWorld world = ctx.getSource().getWorld();
+
+        double radiusX = radius+0.5;
+        double radiusY = radius+0.5;
+        double radiusZ = radius+0.5;
 
         final double invRadiusX = 1 / radiusX;
         final double invRadiusY = 1 / radiusY;
@@ -85,22 +190,28 @@ public class DrawCommand
 
         double nextXn = 0;
 
-        forX: for (int x = 0; x <= ceilRadiusX; ++x) {
+        forX: for (int x = 0; x <= ceilRadiusX; ++x)
+        {
             final double xn = nextXn;
             nextXn = (x + 1) * invRadiusX;
             double nextYn = 0;
-            forY: for (int y = 0; y <= ceilRadiusY; ++y) {
+            forY: for (int y = 0; y <= ceilRadiusY; ++y)
+            {
                 final double yn = nextYn;
                 nextYn = (y + 1) * invRadiusY;
                 double nextZn = 0;
-                forZ: for (int z = 0; z <= ceilRadiusZ; ++z) {
+                forZ: for (int z = 0; z <= ceilRadiusZ; ++z)
+                {
                     final double zn = nextZn;
                     nextZn = (z + 1) * invRadiusZ;
 
                     double distanceSq = lengthSq(xn, yn, zn);
-                    if (distanceSq > 1) {
-                        if (z == 0) {
-                            if (y == 0) {
+                    if (distanceSq > 1)
+                    {
+                        if (z == 0)
+                        {
+                            if (y == 0)
+                            {
                                 break forX;
                             }
                             break forY;
@@ -108,35 +219,22 @@ public class DrawCommand
                         break forZ;
                     }
 
-                    if (!solid) {
-                        if (lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) {
-                            continue;
-                        }
+                    if (!solid && lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1)
+                    {
+                        continue;
                     }
 
                     CarpetSettings.impendingFillSkipUpdates = !CarpetSettings.fillUpdates;
-                    for (int xmod = -1; xmod < 2; xmod+= 2)
+                    for (int xmod = -1; xmod < 2; xmod += 2)
                     {
                         for (int ymod = -1; ymod < 2; ymod += 2)
                         {
                             for (int zmod = -1; zmod < 2; zmod += 2)
                             {
-                                mbpos.set(pos.getX()+xmod*x, pos.getY()+ymod*y, pos.getZ()+zmod*z);
-                                if (replacement == null || replacement.test(
-                                        new CachedBlockPosition( world, mbpos, true)))
-                                {
-                                    BlockEntity tileentity = world.getBlockEntity(mbpos);
-                                    if (tileentity instanceof Inventory)
-                                    {
-                                        ((Inventory)tileentity).clear();
-                                    }
-
-                                    if (block.setBlockState(world, mbpos,2))
-                                    {
-                                        list.add(mbpos.toImmutable());
-                                        ++affected;
-                                    }
-                                }
+                                affected+= setBlock(world, mbpos,
+                                        pos.getX() + xmod * x, pos.getY() + ymod * y, pos.getZ() + zmod * z,
+                                        block, replacement, list
+                                );
                             }
                         }
                     }
@@ -146,19 +244,212 @@ public class DrawCommand
         }
         if (CarpetSettings.fillUpdates)
         {
+            list.forEach(blockpos1 -> world.updateNeighbors(blockpos1, world.getBlockState(blockpos1).getBlock()));
+        }
+        Messenger.m(ctx.getSource(), "gi Filled " + affected + " blocks");
+        return affected;
+    }
 
-            for (BlockPos blockpos1 : list)
+    private static int drawDiamond(CommandContext ctx, boolean solid) throws CommandSyntaxException
+    {
+        BlockPos pos;
+        int radius;
+        BlockStateArgument block;
+        Predicate replacement;
+        try
+        {
+            pos = getArg(ctx, BlockPosArgumentType::getBlockPos, "center");
+            radius = getArg(ctx, IntegerArgumentType::getInteger, "radius");
+            block = getArg(ctx, BlockStateArgumentType::getBlockState, "block");
+            replacement = getArg(ctx, BlockPredicateArgumentType::getBlockPredicate, "filter", true);
+        }
+        catch (ErrorHandled ignored) { return 0; }
+
+        ServerCommandSource source = ctx.getSource();
+
+        int affected=0;
+
+        BlockPos.Mutable mbpos = new BlockPos.Mutable(pos);
+        List list = Lists.newArrayList();
+
+        ServerWorld world = source.getWorld();
+
+        CarpetSettings.impendingFillSkipUpdates = !CarpetSettings.fillUpdates;
+
+        for (int r = 0; r < radius; ++r)
+        {
+            int y=r-radius+1;
+            for (int x = -r; x <= r; ++x)
             {
-                Block blokc = world.getBlockState(blockpos1).getBlock();
-                world.updateNeighbors(blockpos1, blokc); // or always version????
+                int z=r-Math.abs(x);
+
+                affected+= setBlock(world, mbpos, pos.getX()+x, pos.getY()-y, pos.getZ()+z, block, replacement, list);
+                affected+= setBlock(world, mbpos, pos.getX()+x, pos.getY()-y, pos.getZ()-z, block, replacement, list);
+                affected+= setBlock(world, mbpos, pos.getX()+x, pos.getY()+y, pos.getZ()+z, block, replacement, list);
+                affected+= setBlock(world, mbpos, pos.getX()+x, pos.getY()+y, pos.getZ()-z, block, replacement, list);
             }
         }
-        Messenger.m(source, "gi Filled "+affected+" blocks");
 
-        return 1;
-    }
-    private static double lengthSq(double x, double y, double z)
+        CarpetSettings.impendingFillSkipUpdates = false;
+
+        if (CarpetSettings.fillUpdates)
         {
-        return (x * x) + (y * y) + (z * z);
+            list.forEach(p -> world.updateNeighbors(p, world.getBlockState(p).getBlock()));
+        }
+
+        Messenger.m(source, "gi Filled " + affected + " blocks");
+
+        return affected;
+    }
+
+    private static int fillFlat(
+            ServerWorld world, BlockPos pos, int offset, double dr, boolean rectangle, String orientation,
+            BlockStateArgument block, Predicate replacement,
+            List list, BlockPos.Mutable mbpos
+    )
+    {
+        int successes=0;
+        int r = MathHelper.floor(dr);
+        double drsq = dr*dr;
+        if (orientation.equalsIgnoreCase("x"))
+        {
+            for(int a=-r; a<=r; ++a) for(int b=-r; b<=r; ++b) if(rectangle || a*a + b*b <= drsq)
+            {
+                successes += setBlock(
+                        world, mbpos,pos.getX()+offset, pos.getY()+a, pos.getZ()+b,
+                        block, replacement, list
+                );
+            }
+            return successes;
+        }
+        if (orientation.equalsIgnoreCase("y"))
+        {
+            for(int a=-r; a<=r; ++a) for(int b=-r; b<=r; ++b) if(rectangle || a*a + b*b <= drsq)
+            {
+                successes += setBlock(
+                        world, mbpos,pos.getX()+a, pos.getY()+offset, pos.getZ()+b,
+                        block, replacement, list
+                );
+            }
+            return successes;
+        }
+        if (orientation.equalsIgnoreCase("z"))
+        {
+            for(int a=-r; a<=r; ++a) for(int b=-r; b<=r; ++b) if(rectangle || a*a + b*b <= drsq)
+            {
+                successes += setBlock(
+                        world, mbpos,pos.getX()+b, pos.getY()+a, pos.getZ()+offset,
+                        block, replacement, list
+                );
+            }
+            return successes;
+        }
+        return 0;
+    }
+
+    private static int drawPyramid(CommandContext ctx, String base, boolean solid) throws CommandSyntaxException
+    {
+        BlockPos pos;
+        double radius;
+        int height;
+        boolean pointup;
+        String orientation;
+        BlockStateArgument block;
+        Predicate replacement;
+        try
+        {
+            pos = getArg(ctx, BlockPosArgumentType::getBlockPos, "center");
+            radius = getArg(ctx, IntegerArgumentType::getInteger, "radius")+0.5D;
+            height = getArg(ctx, IntegerArgumentType::getInteger, "height");
+            pointup = getArg(ctx, StringArgumentType::getString, "pointing").equalsIgnoreCase("up");
+            orientation = getArg(ctx, StringArgumentType::getString,"orientation");
+            block = getArg(ctx, BlockStateArgumentType::getBlockState, "block");
+            replacement = getArg(ctx, BlockPredicateArgumentType::getBlockPredicate, "filter", true);
+        }
+        catch (ErrorHandled ignored) { return 0; }
+
+        ServerCommandSource source = ctx.getSource();
+
+        int affected = 0;
+        BlockPos.Mutable mbpos = new BlockPos.Mutable(pos);
+
+        List list = Lists.newArrayList();
+
+        ServerWorld world = source.getWorld();
+
+        CarpetSettings.impendingFillSkipUpdates = !CarpetSettings.fillUpdates;
+
+        boolean isSquare = base.equalsIgnoreCase("square");
+
+        for(int i =0; i ctx, String base){
+        BlockPos pos;
+        double radius;
+        int height;
+        String orientation;
+        BlockStateArgument block;
+        Predicate replacement;
+        try
+        {
+            pos = getArg(ctx, BlockPosArgumentType::getBlockPos, "center");
+            radius = getArg(ctx, IntegerArgumentType::getInteger, "radius")+0.5D;
+            height = getArg(ctx, IntegerArgumentType::getInteger, "height");
+            orientation = getArg(ctx, StringArgumentType::getString,"orientation");
+            block = getArg(ctx, BlockStateArgumentType::getBlockState, "block");
+            replacement = getArg(ctx, BlockPredicateArgumentType::getBlockPredicate, "filter", true);
+        }
+        catch (ErrorHandled | CommandSyntaxException ignored) { return 0; }
+
+        ServerCommandSource source = ctx.getSource();
+
+        int affected = 0;
+        BlockPos.Mutable mbpos = new BlockPos.Mutable(pos);
+
+        List list = Lists.newArrayList();
+
+        ServerWorld world = source.getWorld();
+
+        CarpetSettings.impendingFillSkipUpdates = !CarpetSettings.fillUpdates;
+
+        boolean isSquare = base.equalsIgnoreCase("square");
+
+        for(int i =0; i generalMobcaps(c.getSource()) ).
                         then(argument("type", string()).
                                 suggests( (c, b)->suggestMatching(Arrays.stream(EntityCategory.values()).map(EntityCategory::getName), b)).
-                                executes( (c) -> listEntitiesOfType(c.getSource(), getString(c, "type")))));
+                                executes( (c) -> listEntitiesOfType(c.getSource(), getString(c, "type"), false)).
+                                then(literal("all").executes( (c) -> listEntitiesOfType(c.getSource(), getString(c, "type"), true)))));
 
         dispatcher.register(literalargumentbuilder);
     }
@@ -194,7 +196,7 @@ public class SpawnCommand
         TickSpeed.tickrate_advance(null, 0, null, null);
         // tick warp given player
         ServerCommandSource csource = null;
-        PlayerEntity player = null;
+        ServerPlayerEntity player = null;
         try
         {
             player = source.getPlayer();
@@ -262,10 +264,10 @@ public class SpawnCommand
         return 1;
     }
 
-    private static int listEntitiesOfType(ServerCommandSource source, String mobtype) throws CommandSyntaxException
+    private static int listEntitiesOfType(ServerCommandSource source, String mobtype, boolean all) throws CommandSyntaxException
     {
         EntityCategory cat = getCategory(mobtype);
-        Messenger.send(source, SpawnReporter.printEntitiesByType(cat, source.getWorld()));
+        Messenger.send(source, SpawnReporter.printEntitiesByType(cat, source.getWorld(), all));
         return 1;
     }
 }
diff --git a/src/main/java/carpet/commands/TickCommand.java b/src/main/java/carpet/commands/TickCommand.java
index ac65b7bd..e705c8b9 100644
--- a/src/main/java/carpet/commands/TickCommand.java
+++ b/src/main/java/carpet/commands/TickCommand.java
@@ -9,6 +9,7 @@ import com.mojang.brigadier.CommandDispatcher;
 import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 import com.mojang.brigadier.exceptions.CommandSyntaxException;
 import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.text.BaseText;
 import net.minecraft.server.command.ServerCommandSource;
 
@@ -43,7 +44,8 @@ public class TickCommand
                                                 c.getSource(),
                                                 getInteger(c,"ticks"),
                                                 getString(c, "tail command")))))).
-                then(literal("freeze").executes( (c)-> toggleFreeze(c.getSource()))).
+                then(literal("freeze").executes( (c)-> toggleFreeze(c.getSource(), false)).
+                        then(literal("deep").executes( (c)-> toggleFreeze(c.getSource(), true)))).
                 then(literal("step").
                         executes((c) -> step(1)).
                         then(argument("ticks", integer(1,72000)).
@@ -66,7 +68,7 @@ public class TickCommand
 
     private static int setTps(ServerCommandSource source, float tps)
     {
-        TickSpeed.tickrate(tps);
+        TickSpeed.tickrate(tps, true);
         queryTps(source);
         return (int)tps;
     }
@@ -79,7 +81,7 @@ public class TickCommand
 
     private static int setWarp(ServerCommandSource source, int advance, String tail_command)
     {
-        PlayerEntity player = null;
+        ServerPlayerEntity player = null;
         try
         {
             player = source.getPlayer();
@@ -95,15 +97,18 @@ public class TickCommand
         return 1;
     }
 
-    private static int toggleFreeze(ServerCommandSource source)
+    private static int toggleFreeze(ServerCommandSource source, boolean isDeep)
     {
         TickSpeed.is_paused = !TickSpeed.is_paused;
         if (TickSpeed.is_paused)
         {
-            Messenger.m(source, "gi Game is paused");
+            TickSpeed.deepFreeze = isDeep;
+            Messenger.m(source, "gi Game is "+(isDeep?"deeply ":"")+"frozen");
+
         }
         else
         {
+            TickSpeed.deepFreeze = false;
             Messenger.m(source, "gi Game runs normally");
         }
         return 1;
diff --git a/src/main/java/carpet/fakes/ChunkTicketManagerInterface.java b/src/main/java/carpet/fakes/ChunkTicketManagerInterface.java
new file mode 100644
index 00000000..37c4ec65
--- /dev/null
+++ b/src/main/java/carpet/fakes/ChunkTicketManagerInterface.java
@@ -0,0 +1,12 @@
+package carpet.fakes;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.server.world.ChunkTicket;
+import net.minecraft.util.SortedArraySet;
+import net.minecraft.util.math.ChunkPos;
+
+public interface ChunkTicketManagerInterface
+{
+    void changeSpawnChunks(ChunkPos pos, int distance);
+    Long2ObjectOpenHashMap>> getTicketsByPosition();
+}
diff --git a/src/main/java/carpet/fakes/CustomPayloadC2SPacketInterface.java b/src/main/java/carpet/fakes/CustomPayloadC2SPacketInterface.java
new file mode 100644
index 00000000..baa636e7
--- /dev/null
+++ b/src/main/java/carpet/fakes/CustomPayloadC2SPacketInterface.java
@@ -0,0 +1,10 @@
+package carpet.fakes;
+
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
+
+public interface CustomPayloadC2SPacketInterface
+{
+    Identifier getPacketChannel();
+    PacketByteBuf getPacketData();
+}
diff --git a/src/main/java/carpet/fakes/EntityInterface.java b/src/main/java/carpet/fakes/EntityInterface.java
index 3e4713b9..4970d5e7 100644
--- a/src/main/java/carpet/fakes/EntityInterface.java
+++ b/src/main/java/carpet/fakes/EntityInterface.java
@@ -11,4 +11,8 @@ public interface EntityInterface
     boolean isPermanentVehicle();
 
     void setPermanentVehicle(boolean permanent);
+
+    int getPortalTimer();
+
+    void setPortalTimer(int amount);
 }
diff --git a/src/main/java/carpet/fakes/HungerManagerInterface.java b/src/main/java/carpet/fakes/HungerManagerInterface.java
new file mode 100644
index 00000000..b4df2334
--- /dev/null
+++ b/src/main/java/carpet/fakes/HungerManagerInterface.java
@@ -0,0 +1,6 @@
+package carpet.fakes;
+
+public interface HungerManagerInterface {
+    float getExhaustionCM();
+    void setExhaustionCM(float exhaust);
+}
diff --git a/src/main/java/carpet/fakes/ItemEntityInterface.java b/src/main/java/carpet/fakes/ItemEntityInterface.java
index d4b8e839..5faa1f74 100644
--- a/src/main/java/carpet/fakes/ItemEntityInterface.java
+++ b/src/main/java/carpet/fakes/ItemEntityInterface.java
@@ -2,6 +2,6 @@ package carpet.fakes;
 
 public interface ItemEntityInterface
 {
-    int getAge();
-    int getPickupDelay();
+    int getAgeCM();
+    int getPickupDelayCM();
 }
diff --git a/src/main/java/carpet/fakes/LivingEntityInterface.java b/src/main/java/carpet/fakes/LivingEntityInterface.java
new file mode 100644
index 00000000..2dba302e
--- /dev/null
+++ b/src/main/java/carpet/fakes/LivingEntityInterface.java
@@ -0,0 +1,7 @@
+package carpet.fakes;
+
+public interface LivingEntityInterface
+{
+    void doJumpCM(); // added CM suffix to remove potential collisions with other mods
+    boolean isJumpingCM();
+}
diff --git a/src/main/java/carpet/fakes/ServerChunkManagerInterface.java b/src/main/java/carpet/fakes/ServerChunkManagerInterface.java
new file mode 100644
index 00000000..f63cb266
--- /dev/null
+++ b/src/main/java/carpet/fakes/ServerChunkManagerInterface.java
@@ -0,0 +1,8 @@
+package carpet.fakes;
+
+import net.minecraft.server.world.ChunkTicketManager;
+
+public interface ServerChunkManagerInterface
+{
+    ChunkTicketManager getCMTicketManager();
+}
diff --git a/src/main/java/carpet/helpers/BlockRotator.java b/src/main/java/carpet/helpers/BlockRotator.java
index 78dfd716..2d83f824 100644
--- a/src/main/java/carpet/helpers/BlockRotator.java
+++ b/src/main/java/carpet/helpers/BlockRotator.java
@@ -263,6 +263,15 @@ public class BlockRotator
             }
             world.setBlockState(blockpos, iblockstate.with(HorizontalFacingBlock.FACING, face), 3);
         }
+        else if(block == Blocks.HOPPER )
+        {
+            Direction face = iblockstate.get(HopperBlock.FACING);
+            if (face != Direction.DOWN)
+            {
+                face = rotateClockwise(face, Direction.Axis.Y);
+                world.setBlockState(blockpos, iblockstate.with(HopperBlock.FACING, face), 3);
+            }
+        }
         // Send block update to the block that just have been rotated.
         world.updateNeighbor(blockpos, block, source.getBlockPos());
 
@@ -270,8 +279,6 @@ public class BlockRotator
     }
 
 
-
-
     public static boolean flip_block(BlockState state, World world, PlayerEntity player, Hand hand, BlockHitResult hit)
     {
         Block block = state.getBlock();
diff --git a/src/main/java/carpet/helpers/EntityPlayerActionPack.java b/src/main/java/carpet/helpers/EntityPlayerActionPack.java
index 5defa3a1..d33694a2 100644
--- a/src/main/java/carpet/helpers/EntityPlayerActionPack.java
+++ b/src/main/java/carpet/helpers/EntityPlayerActionPack.java
@@ -390,6 +390,7 @@ public class EntityPlayerActionPack
                             {
                                 player.interactionManager.processBlockBreakingAction(pos, PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK, side, player.server.getWorldHeight());
                                 ap.currentBlock = null;
+                                ap.blockHitDelay = 5;
                                 blockBroken = true;
                             }
                             player.world.setBlockBreakingInfo(-1, pos, (int) (ap.curBlockDamageMP * 10));
diff --git a/src/main/java/carpet/helpers/FeatureGenerator.java b/src/main/java/carpet/helpers/FeatureGenerator.java
index eb8caf20..4464ba4c 100644
--- a/src/main/java/carpet/helpers/FeatureGenerator.java
+++ b/src/main/java/carpet/helpers/FeatureGenerator.java
@@ -6,13 +6,19 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import net.minecraft.block.Blocks;
 import net.minecraft.server.world.ServerWorld;
+import net.minecraft.structure.StructureManager;
+import net.minecraft.structure.StructureStart;
+import net.minecraft.util.math.BlockBox;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.ChunkPos;
 import net.minecraft.world.biome.Biome;
 import net.minecraft.world.biome.Biomes;
 import net.minecraft.world.biome.DefaultBiomeFeatures;
+import net.minecraft.world.biome.source.BiomeAccess;
 import net.minecraft.world.biome.source.BiomeSource;
 import net.minecraft.world.biome.source.VanillaLayeredBiomeSource;
 import net.minecraft.world.biome.source.VanillaLayeredBiomeSourceConfig;
+import net.minecraft.world.gen.ChunkRandom;
 import net.minecraft.world.gen.chunk.ChunkGenerator;
 import net.minecraft.world.gen.chunk.OverworldChunkGenerator;
 import net.minecraft.world.gen.chunk.OverworldChunkGeneratorConfig;
@@ -141,26 +147,47 @@ public class FeatureGenerator
         return null;
     }
 
+    public static StructureStart shouldStructureStartAt(ServerWorld world, BlockPos pos, StructureFeature structure, boolean computeBox)
+    {
+        ChunkGenerator generator = world.getChunkManager().getChunkGenerator();
+        if (!generator.getBiomeSource().hasStructureFeature(structure))
+            return null;
+        BiomeAccess biomeAccess = world.getBiomeAccess().withSource(generator.getBiomeSource());
+        ChunkRandom chunkRandom = new ChunkRandom();
+        ChunkPos chunkPos = new ChunkPos(pos);
+        Biome biome = biomeAccess.getBiome(new BlockPos(chunkPos.getStartX() + 9, 0, chunkPos.getStartZ() + 9));
+        if (structure.shouldStartAt(biomeAccess, generator, chunkRandom, chunkPos.x, chunkPos.z, biome))
+        {
+            if (!computeBox) return StructureStart.DEFAULT;
+            StructureManager manager = world.getSaveHandler().getStructureManager();
+            StructureStart structureStart3 = structure.getStructureStartFactory().create(structure, chunkPos.x, chunkPos.z, BlockBox.empty(), 0, generator.getSeed());
+            structureStart3.initialize(generator, manager, chunkPos.x, chunkPos.z, biome);
+            if (!structureStart3.hasChildren()) return null;
+            return structureStart3;
+        }
+        return null;
+    }
+
 
     public static final Map> structureToFeature = new HashMap<>();
     public static final Map featureToStructure = new HashMap<>();
     static
     {
-        structureToFeature.put("Monument", Collections.singletonList("monument"));
-        structureToFeature.put("EndCity", Collections.singletonList("end_city"));
-        structureToFeature.put("Ocean_Ruin", Arrays.asList("ocean_ruin", "ocean_ruin_warm", "ocean_ruin_small", "ocean_ruin_warm_small", "ocean_ruin_tall", "ocean_ruin_warm_tall"));
-        structureToFeature.put("Village", Arrays.asList("village", "village_desert", "village_savanna", "village_taiga", "village_snowy"));
-        structureToFeature.put("Mansion", Collections.singletonList("mansion"));
-        structureToFeature.put("Buried_Treasure", Collections.singletonList("treasure"));
-        structureToFeature.put("Swamp_Hut", Collections.singletonList("witch_hut"));
-        structureToFeature.put("Stronghold", Collections.singletonList("stronghold"));
-        structureToFeature.put("Desert_Pyramid", Collections.singletonList("desert_temple"));
-        structureToFeature.put("Jungle_Pyramid", Collections.singletonList("jungle_temple"));
-        structureToFeature.put("Shipwreck", Arrays.asList("shipwreck", "shipwreck2"));
-        structureToFeature.put("Pillager_Outpost", Collections.singletonList("pillager_outpost"));
-        structureToFeature.put("Mineshaft", Arrays.asList("mineshaft", "mineshaft_mesa"));
-        structureToFeature.put("Igloo", Collections.singletonList("igloo"));
-        structureToFeature.put("Fortress", Collections.singletonList("fortress"));
+        structureToFeature.put(Feature.OCEAN_MONUMENT.getName(), Collections.singletonList("monument"));
+        structureToFeature.put(Feature.END_CITY.getName(), Collections.singletonList("end_city"));
+        structureToFeature.put(Feature.OCEAN_RUIN.getName(), Arrays.asList("ocean_ruin", "ocean_ruin_warm", "ocean_ruin_small", "ocean_ruin_warm_small", "ocean_ruin_tall", "ocean_ruin_warm_tall"));
+        structureToFeature.put(Feature.VILLAGE.getName(), Arrays.asList("village", "village_desert", "village_savanna", "village_taiga", "village_snowy"));
+        structureToFeature.put(Feature.WOODLAND_MANSION.getName(), Collections.singletonList("mansion"));
+        structureToFeature.put(Feature.BURIED_TREASURE.getName(), Collections.singletonList("treasure"));
+        structureToFeature.put(Feature.SWAMP_HUT.getName(), Collections.singletonList("witch_hut"));
+        structureToFeature.put(Feature.STRONGHOLD.getName(), Collections.singletonList("stronghold"));
+        structureToFeature.put(Feature.DESERT_PYRAMID.getName(), Collections.singletonList("desert_temple"));
+        structureToFeature.put(Feature.JUNGLE_TEMPLE.getName(), Collections.singletonList("jungle_temple"));
+        structureToFeature.put(Feature.SHIPWRECK.getName(), Arrays.asList("shipwreck", "shipwreck2"));
+        structureToFeature.put(Feature.PILLAGER_OUTPOST.getName(), Collections.singletonList("pillager_outpost"));
+        structureToFeature.put(Feature.MINESHAFT.getName(), Arrays.asList("mineshaft", "mineshaft_mesa"));
+        structureToFeature.put(Feature.IGLOO.getName(), Collections.singletonList("igloo"));
+        structureToFeature.put(Feature.NETHER_BRIDGE.getName(), Collections.singletonList("fortress"));
 
         structureToFeature.forEach((key, value) -> value.forEach(el -> featureToStructure.put(el, key)));
     }
diff --git a/src/main/java/carpet/helpers/HopperCounter.java b/src/main/java/carpet/helpers/HopperCounter.java
index d31876a4..4c7163c5 100644
--- a/src/main/java/carpet/helpers/HopperCounter.java
+++ b/src/main/java/carpet/helpers/HopperCounter.java
@@ -126,7 +126,7 @@ public class HopperCounter
                 color, ticks*1.0/(20*60), (realTime?" - real time":""), total, total*1.0*(20*60*60)/ticks),
                 "nb [X]", "^g reset", "!/counter "+color+" reset"
         ));
-        items.addAll(counter.object2LongEntrySet().stream().map(e ->
+        items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(e ->
         {
             BaseText itemName = new TranslatableText(e.getKey().getTranslationKey());
             long count = e.getLongValue();
diff --git a/src/main/java/carpet/helpers/OptimizedExplosion.java b/src/main/java/carpet/helpers/OptimizedExplosion.java
index a3d0e8ec..df6a463e 100644
--- a/src/main/java/carpet/helpers/OptimizedExplosion.java
+++ b/src/main/java/carpet/helpers/OptimizedExplosion.java
@@ -7,8 +7,10 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import carpet.logging.logHelpers.ExplosionLogHelper;
 import carpet.mixins.ExplosionAccessor;
 import carpet.CarpetSettings;
+import carpet.mixins.ExplosionMixin;
 import carpet.utils.Messenger;
 import com.google.common.collect.Lists;
 import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap;
@@ -51,6 +53,7 @@ public class OptimizedExplosion
     private static List entitylist;
     private static Vec3d vec3dmem;
     private static long tickmem;
+    private static Vec3d vec3dZero = new Vec3d(0, 0, 0);
     // For disabling the explosion particles and sound
     public static int explosionSound = 0;
 
@@ -66,7 +69,7 @@ public class OptimizedExplosion
     private static ArrayList chances = new ArrayList<>();
     private static BlockPos blastChanceLocation;
 
-    public static void doExplosionA(Explosion e) {
+    public static void doExplosionA(Explosion e, ExplosionLogHelper eLogger) {
         ExplosionAccessor eAccess = (ExplosionAccessor) e;
         
         blastCalc(e);
@@ -120,6 +123,9 @@ public class OptimizedExplosion
                     entity.getX() == eAccess.getEntity().getX() &&
                     entity.getY() == eAccess.getEntity().getY() &&
                     entity.getZ() == eAccess.getEntity().getZ()) {
+                if (eLogger != null) {
+                    eLogger.onEntityImpacted(entity, new Vec3d(0,-0.9923437498509884d, 0));
+                }
                 continue;
             }
 
@@ -158,6 +164,10 @@ public class OptimizedExplosion
                             d11 = ProtectionEnchantment.transformExplosionKnockback((LivingEntity) entity, d10);
                         }
 
+                        if (eLogger != null) {
+                            eLogger.onEntityImpacted(entity, new Vec3d(d5 * d11, d7 * d11, d9 * d11));
+                        }
+
                         entity.setVelocity(entity.getVelocity().add(d5 * d11, d7 * d11, d9 * d11));
 
                         if (entity instanceof PlayerEntity) {
diff --git a/src/main/java/carpet/helpers/TickSpeed.java b/src/main/java/carpet/helpers/TickSpeed.java
index 79d05576..575867eb 100644
--- a/src/main/java/carpet/helpers/TickSpeed.java
+++ b/src/main/java/carpet/helpers/TickSpeed.java
@@ -5,11 +5,12 @@ import java.util.Map;
 import java.util.function.BiConsumer;
 
 import carpet.CarpetServer;
+import carpet.network.ServerNetworkHandler;
 import carpet.utils.Messenger;
-import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.CommandManager;
 import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.text.BaseText;
 
 public class TickSpeed
@@ -20,11 +21,12 @@ public class TickSpeed
     public static long time_bias = 0;
     public static long time_warp_start_time = 0;
     public static long time_warp_scheduled_ticks = 0;
-    public static PlayerEntity time_advancerer = null;
+    public static ServerPlayerEntity time_advancerer = null;
     public static String tick_warp_callback = null;
     public static ServerCommandSource tick_warp_sender = null;
     public static int player_active_timeout = 0;
     public static boolean process_entities = true;
+    public static boolean deepFreeze = false;
     public static boolean is_paused = false;
     public static boolean is_superHot = false;
 
@@ -43,18 +45,37 @@ public class TickSpeed
         }
     }
 
+    public static void reset()
+    {
+        tickrate = 20.0f;
+        mspt = 50.0f;
+        time_bias = 0;
+        time_warp_start_time = 0;
+        time_warp_scheduled_ticks = 0;
+        time_advancerer = null;
+        tick_warp_callback = null;
+        tick_warp_sender = null;
+        player_active_timeout = 0;
+        process_entities = true;
+        deepFreeze = false;
+        is_paused = false;
+        is_superHot = false;
+        notifyTickrateListeners("carpet");
+    }
+
     public static void add_ticks_to_run_in_pause(int ticks)
     {
         player_active_timeout = PLAYER_GRACE+ticks;
     }
 
-    public static BaseText tickrate_advance(PlayerEntity player, int advance, String callback, ServerCommandSource source)
+    public static BaseText tickrate_advance(ServerPlayerEntity player, int advance, String callback, ServerCommandSource source)
     {
         if (0 == advance)
         {
             tick_warp_callback = null;
-            tick_warp_sender = null;
+            if (source != tick_warp_sender) tick_warp_sender = null;
             finish_time_warp();
+            tick_warp_sender = null;
             return Messenger.c("gi Warp interrupted");
         }
         if (time_bias > 0)
@@ -157,8 +178,9 @@ public class TickSpeed
             }
         }
     }
-    
-    public static void tickrate(float rate)
+    //unused - mod compat reasons
+    public static void tickrate(float rate) {tickrate(rate, true);}
+    public static void tickrate(float rate, boolean update)
     {
         tickrate = rate;
         long mspt = (long)(1000.0 / tickrate);
@@ -170,7 +192,7 @@ public class TickSpeed
         
         TickSpeed.mspt = (float)mspt;
         
-        notifyTickrateListeners("carpet");
+        if (update) notifyTickrateListeners("carpet");
     }
     
     private static void tickrateChanged(String modId, float rate)
@@ -202,6 +224,7 @@ public class TickSpeed
 	            }
 	        }
         }
+        ServerNetworkHandler.updateTickSpeedToConnectedPlayers();
     }
     
     public static BiConsumer addTickrateListener(String modId, BiConsumer tickrateListener) 
diff --git a/src/main/java/carpet/logging/LoggerRegistry.java b/src/main/java/carpet/logging/LoggerRegistry.java
index 6034a702..9d397c59 100644
--- a/src/main/java/carpet/logging/LoggerRegistry.java
+++ b/src/main/java/carpet/logging/LoggerRegistry.java
@@ -30,6 +30,7 @@ public class LoggerRegistry
     public static boolean __packets;
     public static boolean __weather;
     public static boolean __pathfinding;
+    public static boolean __explosions;
 
     public static void initLoggers()
     {
@@ -52,6 +53,7 @@ public class LoggerRegistry
         registerLogger("packets", HUDLogger.stardardHUDLogger("packets", null, null));
         registerLogger("counter",HUDLogger.stardardHUDLogger("counter","white", Arrays.stream(DyeColor.values()).map(Object::toString).toArray(String[]::new)));
         registerLogger("mobcaps", HUDLogger.stardardHUDLogger("mobcaps", "dynamic",new String[]{"dynamic", "overworld", "nether","end"}));
+        registerLogger("explosions", HUDLogger.stardardLogger("explosions", "brief",new String[]{"brief", "full"}));
 
     }
 
diff --git a/src/main/java/carpet/logging/logHelpers/ExplosionLogHelper.java b/src/main/java/carpet/logging/logHelpers/ExplosionLogHelper.java
new file mode 100644
index 00000000..e11479e5
--- /dev/null
+++ b/src/main/java/carpet/logging/logHelpers/ExplosionLogHelper.java
@@ -0,0 +1,125 @@
+package carpet.logging.logHelpers;
+
+import carpet.logging.LoggerRegistry;
+import carpet.utils.Messenger;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityType;
+import net.minecraft.text.BaseText;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.registry.Registry;
+import net.minecraft.world.explosion.Explosion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static carpet.utils.Messenger.c;
+
+public class ExplosionLogHelper
+{
+    private final boolean createFire;
+    private final Explosion.DestructionType blockDestructionType;
+    public final Vec3d pos;
+    public final Entity entity;
+    private final float power;
+    private boolean affectBlocks = false;
+    private Object2IntMap impactedEntities = new Object2IntOpenHashMap<>();
+
+    private static long lastGametime = 0;
+    private static int explosionCountInCurretGT = 0;
+
+    public ExplosionLogHelper(Entity entity, double x, double y, double z, float power, boolean createFire, Explosion.DestructionType blockDestructionType) {
+        this.entity = entity;
+        this.power = power;
+        this.pos = new Vec3d(x,y,z);
+        this.createFire = createFire;
+        this.blockDestructionType = blockDestructionType;
+    }
+
+    public void setAffectBlocks(boolean b)
+    {
+        affectBlocks = b;
+    }
+
+    public void onExplosionDone(long gametime)
+    {
+        List messages = new ArrayList<>();
+        if (!(lastGametime == gametime)){
+            explosionCountInCurretGT = 0;
+            lastGametime = gametime;
+            messages.add(c("wb tick : ", "d " + gametime));
+        }
+        explosionCountInCurretGT++;
+        LoggerRegistry.getLogger("explosions").log( (option) -> {
+            if ("brief".equals(option))
+            {
+                messages.add( c("d #" + explosionCountInCurretGT,"gb ->",
+                        Messenger.dblt("l", pos.x, pos.y, pos.z), (affectBlocks)?"m  (affects blocks)":"m  (doesn't affect blocks)" ));
+            }
+            if ("full".equals(option))
+            {
+                messages.add( c("d #" + explosionCountInCurretGT,"gb ->", Messenger.dblt("l", pos.x, pos.y, pos.z) ));
+                messages.add(c("w   affects blocks: ", "m " + this.affectBlocks));
+                messages.add(c("w   creates fire: ", "m " + this.createFire));
+                messages.add(c("w   power: ", "c " + this.power));
+                messages.add(c( "w   destruction: ",   "c " + this.blockDestructionType.name()));
+                if (impactedEntities.isEmpty())
+                {
+                    messages.add(c("w   affected entities: ", "m None"));
+                }
+                else
+                {
+                    messages.add(c("w   affected entities:"));
+                    impactedEntities.forEach((k, v) ->
+                    {
+                        messages.add(c((k.pos.equals(pos))?"r   - TNT":"w   - ",
+                                Messenger.dblt((k.pos.equals(pos))?"r":"y", k.pos.x, k.pos.y, k.pos.z), "w  dV",
+                                Messenger.dblt("d", k.accel.x, k.accel.y, k.accel.z),
+                                "w  "+Registry.ENTITY_TYPE.getId(k.type).getPath(), (v>1)?"l ("+v+")":""
+                        ));
+                    });
+                }
+            }
+            return messages.toArray(new BaseText[0]);
+        });
+    }
+
+    public void onEntityImpacted(Entity entity, Vec3d accel)
+    {
+        EntityChangedStatusWithCount ent = new EntityChangedStatusWithCount(entity, accel);
+        impactedEntities.put(ent, impactedEntities.getOrDefault(ent, 0)+1);
+    }
+
+
+    public static class EntityChangedStatusWithCount
+    {
+        public final Vec3d pos;
+        public final EntityType type;
+        public final Vec3d accel;
+
+        public EntityChangedStatusWithCount(Entity e, Vec3d accel)
+        {
+            this.pos = e.getPos();
+            this.type = e.getType();
+            this.accel = accel;
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (obj instanceof EntityChangedStatusWithCount)
+            {
+                EntityChangedStatusWithCount other = (EntityChangedStatusWithCount) obj;
+                return other.pos.equals(pos) && other.accel.equals(accel) && other.type.equals(type);
+            }
+            return super.equals(obj);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return pos.hashCode()+ type.hashCode()+accel.hashCode();
+        }
+    }
+}
diff --git a/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java b/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java
index 7e51f743..6b7b35fa 100644
--- a/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java
+++ b/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java
@@ -9,6 +9,8 @@ public class TNTLogHelper
 {
     public boolean initialized;
     private double primedX, primedY, primedZ;
+    private static long lastGametime = 0;
+    private static int tntCount = 0;
     private Vec3d primedAngle;
     /**
      * Runs when the TNT is primed. Expects the position and motion angle of the TNT.
@@ -24,8 +26,13 @@ public class TNTLogHelper
     /**
      * Runs when the TNT explodes. Expects the position of the TNT.
      */
-    public void onExploded(double x, double y, double z)
+    public void onExploded(double x, double y, double z, long gametime)
     {
+        if (!(lastGametime == gametime)){
+            tntCount = 0;
+            lastGametime = gametime;
+        }
+        tntCount++;
         LoggerRegistry.getLogger("tnt").log( (option) -> {
             switch (option)
             {
@@ -36,6 +43,9 @@ public class TNTLogHelper
                             "r  E ",Messenger.dblt("r",x, y, z))};
                 case "full":
                     return new BaseText[]{Messenger.c(
+                            "r #" + String.valueOf(tntCount),
+                            "m @" + String.valueOf(gametime),
+                            "g : ",
                             "l P ",Messenger.dblf("l",primedX,primedY,primedZ),
                             "w  ",Messenger.dblf("l", primedAngle.x, primedAngle.y, primedAngle.z),
                             "r  E ",Messenger.dblf("r",x, y, z))};
diff --git a/src/main/java/carpet/mixins/BlockItem_creativeNoClipMixin.java b/src/main/java/carpet/mixins/BlockItem_creativeNoClipMixin.java
new file mode 100644
index 00000000..f8c84c69
--- /dev/null
+++ b/src/main/java/carpet/mixins/BlockItem_creativeNoClipMixin.java
@@ -0,0 +1,36 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.EntityContext;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.BlockItem;
+import net.minecraft.item.ItemPlacementContext;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.world.World;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(BlockItem.class)
+public class BlockItem_creativeNoClipMixin
+{
+    @Redirect(method = "canPlace", at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/world/World;canPlace(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/entity/EntityContext;)Z"
+    ))
+    private boolean canSpectatingPlace(World world, BlockState state, BlockPos pos, EntityContext context,
+                                       ItemPlacementContext contextOuter, BlockState stateOuter)
+    {
+        PlayerEntity player = contextOuter.getPlayer();
+        if (CarpetSettings.creativeNoClip && player != null && player.isCreative() && player.abilities.flying)
+        {
+            // copy from canPlace
+            VoxelShape voxelShape = state.getCollisionShape(world, pos, context);
+            return voxelShape.isEmpty() || world.intersectsEntities(player, voxelShape.offset(pos.getX(), pos.getY(), pos.getZ()));
+
+        }
+        return world.canPlace(state, pos, context);
+    }
+}
diff --git a/src/main/java/carpet/mixins/ChunkMixin.java b/src/main/java/carpet/mixins/ChunkMixin.java
index 92eb8dd7..b1a9b128 100644
--- a/src/main/java/carpet/mixins/ChunkMixin.java
+++ b/src/main/java/carpet/mixins/ChunkMixin.java
@@ -11,6 +11,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 @Mixin(Chunk.class)
 public abstract class ChunkMixin
 {
+    // failed to mixin into interface. need to mixin in two places that uses it
     @Inject(method = "markBlockForPostProcessing(Lnet/minecraft/util/math/BlockPos;)V",
         at =@At("HEAD"), cancellable = true)
     private void squashWarnings(BlockPos blockPos_1, CallbackInfo ci)
diff --git a/src/main/java/carpet/mixins/ChunkRegion_scarpetPlopMixin.java b/src/main/java/carpet/mixins/ChunkRegion_scarpetPlopMixin.java
new file mode 100644
index 00000000..fb06e695
--- /dev/null
+++ b/src/main/java/carpet/mixins/ChunkRegion_scarpetPlopMixin.java
@@ -0,0 +1,19 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.ChunkRegion;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ChunkRegion.class)
+public class ChunkRegion_scarpetPlopMixin
+{
+    @Inject(method = "markBlockForPostProcessing", at = @At("HEAD"))
+    private void markOrNot(BlockPos blockPos, CallbackInfo ci)
+    {
+        if (CarpetSettings.skipGenerationChecks) ci.cancel();
+    }
+}
diff --git a/src/main/java/carpet/mixins/ChunkTicketManager_scarpetMixin.java b/src/main/java/carpet/mixins/ChunkTicketManager_scarpetMixin.java
index 6d52eebd..72621cdd 100644
--- a/src/main/java/carpet/mixins/ChunkTicketManager_scarpetMixin.java
+++ b/src/main/java/carpet/mixins/ChunkTicketManager_scarpetMixin.java
@@ -1,15 +1,23 @@
 package carpet.mixins;
 
+import carpet.fakes.ChunkTicketManagerInterface;
 import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
 import net.minecraft.server.world.ChunkTicket;
 import net.minecraft.server.world.ChunkTicketManager;
 import net.minecraft.util.SortedArraySet;
+import org.spongepowered.asm.mixin.Final;
 import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.Shadow;
 
 @Mixin(ChunkTicketManager.class)
-public interface ChunkTicketManager_scarpetMixin
+public abstract class ChunkTicketManager_scarpetMixin implements ChunkTicketManagerInterface
 {
-    @Accessor
-    Long2ObjectOpenHashMap>>  getTicketsByPosition();
+    @Shadow @Final private Long2ObjectOpenHashMap>> ticketsByPosition;
+
+    @Override
+    public Long2ObjectOpenHashMap>>  getTicketsByPosition()
+    {
+        return ticketsByPosition;
+    }
+
 }
diff --git a/src/main/java/carpet/mixins/ChunkTicketManager_spawnChunksMixin.java b/src/main/java/carpet/mixins/ChunkTicketManager_spawnChunksMixin.java
new file mode 100644
index 00000000..53987e95
--- /dev/null
+++ b/src/main/java/carpet/mixins/ChunkTicketManager_spawnChunksMixin.java
@@ -0,0 +1,60 @@
+package carpet.mixins;
+
+import carpet.fakes.ChunkTicketManagerInterface;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.server.world.ChunkTicket;
+import net.minecraft.server.world.ChunkTicketManager;
+import net.minecraft.server.world.ChunkTicketType;
+import net.minecraft.util.SortedArraySet;
+import net.minecraft.util.Unit;
+import net.minecraft.util.math.ChunkPos;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+import java.util.Iterator;
+
+@Mixin(ChunkTicketManager.class)
+public abstract class ChunkTicketManager_spawnChunksMixin implements ChunkTicketManagerInterface
+{
+
+    @Shadow protected abstract SortedArraySet> getTicketSet(long position);
+
+    @Shadow @Final private Long2ObjectOpenHashMap>> ticketsByPosition;
+
+    @Shadow public abstract  void removeTicket(ChunkTicketType type, ChunkPos pos, int radius, T argument);
+
+    @Shadow protected abstract void removeTicket(long pos, ChunkTicket ticket);
+
+    @Shadow public abstract  void addTicket(ChunkTicketType type, ChunkPos pos, int radius, T argument);
+
+    @Override
+    public void changeSpawnChunks(ChunkPos chunkPos,  int distance)
+    {
+        long pos = chunkPos.toLong();
+        SortedArraySet> set = ticketsByPosition.get(pos);
+        ChunkTicket existingTicket = null;
+        if (set != null)
+        {
+            Iterator> iter = set.iterator();
+            while(iter.hasNext())
+            {
+                ChunkTicket ticket = iter.next();
+                if (ticket.getType() == ChunkTicketType.START)
+                {
+                    existingTicket = ticket;
+                    iter.remove();
+                }
+            }
+            set.add(existingTicket);
+        }
+        // the reason we are removing the ticket this way is that there are sideeffects of removal
+        if (existingTicket != null)
+        {
+            removeTicket(pos, existingTicket);
+        }
+        // set optionally new spawn ticket
+        if (distance > 0)
+            addTicket(ChunkTicketType.START, chunkPos, distance, Unit.INSTANCE);
+    }
+}
diff --git a/src/main/java/carpet/mixins/ChunkTicketManager_tickMixin.java b/src/main/java/carpet/mixins/ChunkTicketManager_tickMixin.java
new file mode 100644
index 00000000..9b70b707
--- /dev/null
+++ b/src/main/java/carpet/mixins/ChunkTicketManager_tickMixin.java
@@ -0,0 +1,23 @@
+package carpet.mixins;
+
+import carpet.helpers.TickSpeed;
+import net.minecraft.server.world.ChunkTicketManager;
+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;
+
+@Mixin(ChunkTicketManager.class)
+public class ChunkTicketManager_tickMixin
+{
+    @Shadow private long age;
+
+    @Inject(method = "purge", at = @At("HEAD"), cancellable = true)
+    private void pauseTicketSystem(CallbackInfo ci)
+    {
+        // pausing expiry of tickets
+        // that will prevent also chunks from unloading, so require a deep frozen state
+        if (!TickSpeed.process_entities && TickSpeed.deepFreeze) age--;
+    }
+}
diff --git a/src/main/java/carpet/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/carpet/mixins/ClientPlayNetworkHandlerMixin.java
new file mode 100644
index 00000000..362828ea
--- /dev/null
+++ b/src/main/java/carpet/mixins/ClientPlayNetworkHandlerMixin.java
@@ -0,0 +1,43 @@
+package carpet.mixins;
+
+import carpet.network.CarpetClient;
+import carpet.network.ClientNetworkHandler;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket;
+import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket;
+import net.minecraft.text.Text;
+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;
+
+@Mixin(ClientPlayNetworkHandler.class)
+public abstract class ClientPlayNetworkHandlerMixin
+{
+    @Shadow private MinecraftClient client;
+
+    @Inject(method = "onCustomPayload", at = @At("HEAD"), cancellable = true)
+    private void onOnCustomPayload(CustomPayloadS2CPacket packet, CallbackInfo ci)
+    {
+        if (CarpetClient.CARPET_CHANNEL.equals(packet.getChannel()))
+        {
+            ClientNetworkHandler.handleData(packet.getData(), client.player);
+            ci.cancel();
+        }
+    }
+
+    @Inject(method = "onGameJoin", at = @At("RETURN"))
+    private void onGameJoined(GameJoinS2CPacket packet, CallbackInfo info)
+    {
+        CarpetClient.gameJoined(client.player);
+    }
+
+    @Inject(method = "onDisconnected", at = @At("HEAD"))
+    private void onCMDisconnected(Text reason, CallbackInfo ci)
+    {
+        CarpetClient.disconnect();
+    }
+
+}
diff --git a/src/main/java/carpet/mixins/ClientPlayerEntity_clientCommandMixin.java b/src/main/java/carpet/mixins/ClientPlayerEntity_clientCommandMixin.java
index 622de0c3..16a41708 100644
--- a/src/main/java/carpet/mixins/ClientPlayerEntity_clientCommandMixin.java
+++ b/src/main/java/carpet/mixins/ClientPlayerEntity_clientCommandMixin.java
@@ -1,6 +1,7 @@
 package carpet.mixins;
 
 import carpet.CarpetServer;
+import carpet.network.CarpetClient;
 import net.minecraft.client.network.ClientPlayerEntity;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
@@ -13,7 +14,10 @@ public class ClientPlayerEntity_clientCommandMixin
     @Inject(method = "sendChatMessage", at = @At("HEAD"))
     private void inspectMessage(String string, CallbackInfo ci)
     {
-        if (CarpetServer.minecraft_server == null)
-            CarpetServer.settingsManager.inspectClientsideCommand(string);
+        if (CarpetServer.minecraft_server == null && !CarpetClient.isCarpet())
+        {
+            ClientPlayerEntity playerSource = (ClientPlayerEntity)(Object) this;
+            CarpetServer.settingsManager.inspectClientsideCommand(playerSource.getCommandSource(), string);
+        }
     }
 }
diff --git a/src/main/java/carpet/mixins/CustomPayloadC2SPacketMixin.java b/src/main/java/carpet/mixins/CustomPayloadC2SPacketMixin.java
new file mode 100644
index 00000000..0465c72e
--- /dev/null
+++ b/src/main/java/carpet/mixins/CustomPayloadC2SPacketMixin.java
@@ -0,0 +1,27 @@
+package carpet.mixins;
+
+import carpet.fakes.CustomPayloadC2SPacketInterface;
+import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(CustomPayloadC2SPacket.class)
+public class CustomPayloadC2SPacketMixin implements CustomPayloadC2SPacketInterface
+{
+    @Shadow
+    private Identifier channel;
+    @Shadow
+    private PacketByteBuf data;
+
+    @Override
+    public Identifier getPacketChannel() {
+        return channel;
+    }
+
+    @Override
+    public PacketByteBuf getPacketData() {
+        return new PacketByteBuf(this.data.copy());
+    }
+}
diff --git a/src/main/java/carpet/mixins/DamageTracker_scarpetEventMixin.java b/src/main/java/carpet/mixins/DamageTracker_scarpetEventMixin.java
index 6f6a7d62..3ac352d9 100644
--- a/src/main/java/carpet/mixins/DamageTracker_scarpetEventMixin.java
+++ b/src/main/java/carpet/mixins/DamageTracker_scarpetEventMixin.java
@@ -5,6 +5,7 @@ import carpet.script.EntityEventsGroup;
 import net.minecraft.entity.LivingEntity;
 import net.minecraft.entity.damage.DamageSource;
 import net.minecraft.entity.damage.DamageTracker;
+import net.minecraft.server.network.ServerPlayerEntity;
 import org.spongepowered.asm.mixin.Final;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.Shadow;
@@ -12,8 +13,11 @@ import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
+import static carpet.script.CarpetEventServer.Event.PLAYER_DEALS_DAMAGE;
+import static carpet.script.CarpetEventServer.Event.PLAYER_TAKES_DAMAGE;
+
 @Mixin(DamageTracker.class)
-public class DamageTracker_scarpetEventMixin
+public abstract class DamageTracker_scarpetEventMixin
 {
     @Shadow @Final private LivingEntity entity;
 
@@ -21,5 +25,13 @@ public class DamageTracker_scarpetEventMixin
     private void onDamageTaken(DamageSource damageSource_1, float float_1, float float_2, CallbackInfo ci)
     {
         ((EntityInterface)entity).getEventContainer().onEvent(EntityEventsGroup.EntityEventType.ON_DAMAGE, entity, float_2, damageSource_1);
+        if (entity instanceof ServerPlayerEntity && PLAYER_TAKES_DAMAGE.isNeeded())
+        {
+            PLAYER_TAKES_DAMAGE.onDamage(entity, float_2, damageSource_1);
+        }
+        if (damageSource_1.getAttacker() instanceof ServerPlayerEntity && PLAYER_DEALS_DAMAGE.isNeeded())
+        {
+            PLAYER_DEALS_DAMAGE.onDamage(entity, float_2, damageSource_1);
+        }
     }
 }
diff --git a/src/main/java/carpet/mixins/DebugRenderer_scarpetRenderMixin.java b/src/main/java/carpet/mixins/DebugRenderer_scarpetRenderMixin.java
new file mode 100644
index 00000000..0ed9e836
--- /dev/null
+++ b/src/main/java/carpet/mixins/DebugRenderer_scarpetRenderMixin.java
@@ -0,0 +1,19 @@
+package carpet.mixins;
+
+import carpet.network.CarpetClient;
+import net.minecraft.client.render.debug.DebugRenderer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(DebugRenderer.class)
+public class DebugRenderer_scarpetRenderMixin
+{
+    @Inject(method = "reset", at = @At("HEAD"))
+    private void resetScarpetRenderes(CallbackInfo ci)
+    {
+        if (CarpetClient.shapes != null)
+            CarpetClient.shapes.reset();
+    }
+}
diff --git a/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java b/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java
index c4193054..45a8afcf 100644
--- a/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java
+++ b/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java
@@ -14,6 +14,7 @@ public abstract class Entity_scarpetEventsMixin implements EntityInterface
 {
     @Shadow public boolean removed;
 
+    @Shadow protected int netherPortalTime;
     private boolean permanentVehicle;
 
     private final EntityEventsGroup events = new EntityEventsGroup();
@@ -36,6 +37,18 @@ public abstract class Entity_scarpetEventsMixin implements EntityInterface
         permanentVehicle = permanent;
     }
 
+    @Override
+    public int getPortalTimer()
+    {
+        return netherPortalTime;
+    }
+
+    @Override
+    public void setPortalTimer(int amount)
+    {
+        netherPortalTime = amount;
+    }
+
     @Inject(method = "tick", at = @At("HEAD"))
     private void onTickCall(CallbackInfo ci)
     {
diff --git a/src/main/java/carpet/mixins/ExplosionMixin.java b/src/main/java/carpet/mixins/ExplosionMixin.java
index 8e74cda1..b2358119 100644
--- a/src/main/java/carpet/mixins/ExplosionMixin.java
+++ b/src/main/java/carpet/mixins/ExplosionMixin.java
@@ -2,9 +2,13 @@ package carpet.mixins;
 
 import carpet.helpers.OptimizedExplosion;
 import carpet.CarpetSettings;
+import carpet.logging.LoggerRegistry;
+import carpet.logging.logHelpers.ExplosionLogHelper;
 import net.minecraft.block.BlockState;
 import net.minecraft.block.Blocks;
+import net.minecraft.entity.Entity;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 import net.minecraft.world.explosion.Explosion;
 import org.spongepowered.asm.mixin.Final;
@@ -24,13 +28,17 @@ public abstract class ExplosionMixin
     @Final
     private List affectedBlocks;
 
+    @Shadow @Final private World world;
+
+    private ExplosionLogHelper eLogger;
+
     @Inject(method = "collectBlocksAndDamageEntities", at = @At("HEAD"),
             cancellable = true)
     private void onExplosionA(CallbackInfo ci)
     {
         if (CarpetSettings.optimizedTNT)
         {
-            OptimizedExplosion.doExplosionA((Explosion) (Object) this);
+            OptimizedExplosion.doExplosionA((Explosion) (Object) this, eLogger);
             ci.cancel();
         }
     }
@@ -39,6 +47,11 @@ public abstract class ExplosionMixin
             cancellable = true)
     private void onExplosionB(boolean spawnParticles, CallbackInfo ci)
     {
+        if (eLogger != null)
+        {
+            eLogger.setAffectBlocks( ! affectedBlocks.isEmpty());
+            eLogger.onExplosionDone(this.world.getTime());
+        }
         if (CarpetSettings.explosionNoBlockDamage)
         {
             affectedBlocks.clear();
@@ -58,4 +71,24 @@ public abstract class ExplosionMixin
         if (CarpetSettings.explosionNoBlockDamage) return Blocks.BEDROCK.getDefaultState();
         return world.getBlockState(pos);
     }
+
+    @Inject(method = "(Lnet/minecraft/world/World;Lnet/minecraft/entity/Entity;DDDFZLnet/minecraft/world/explosion/Explosion$DestructionType;)V",
+            at = @At(value = "RETURN"))
+    private void onExplosionCreated(World world, Entity entity, double x, double y, double z, float power, boolean createFire, Explosion.DestructionType blockDestructionType, CallbackInfo ci)
+    {
+        if (LoggerRegistry.__explosions && ! world.isClient)
+        {
+            eLogger = new ExplosionLogHelper(entity, x, y, z, power, createFire, blockDestructionType);
+        }
+    }
+
+    @Redirect(method = "collectBlocksAndDamageEntities",
+            at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setVelocity(Lnet/minecraft/util/math/Vec3d;)V"))
+    private void setVelocityAndUpdateLogging(Entity entity, Vec3d velocity)
+    {
+        if (eLogger != null) {
+            eLogger.onEntityImpacted(entity, velocity.subtract(entity.getVelocity()));
+        }
+        entity.setVelocity(velocity);
+    }
 }
diff --git a/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java b/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java
new file mode 100644
index 00000000..3e6fcdc2
--- /dev/null
+++ b/src/main/java/carpet/mixins/HungerManagerMixin_scarpetEventsMixin.java
@@ -0,0 +1,20 @@
+package carpet.mixins;
+
+import carpet.fakes.HungerManagerInterface;
+import net.minecraft.entity.player.HungerManager;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+@Mixin(HungerManager.class)
+public class HungerManagerMixin_scarpetEventsMixin implements HungerManagerInterface {
+    @Shadow private float exhaustion;
+    public float getExhaustionCM(){
+        return exhaustion;
+    }
+
+    @Override
+    public void setExhaustionCM(float exhaust)
+    {
+        exhaustion = exhaust;
+    }
+}
diff --git a/src/main/java/carpet/mixins/ItemEntityMixin.java b/src/main/java/carpet/mixins/ItemEntityMixin.java
index dcc185ee..3c4620c5 100644
--- a/src/main/java/carpet/mixins/ItemEntityMixin.java
+++ b/src/main/java/carpet/mixins/ItemEntityMixin.java
@@ -30,12 +30,12 @@ public abstract class ItemEntityMixin extends Entity implements ItemEntityInterf
     }
 
     @Override
-    public int getAge() {
+    public int getAgeCM() {
         return this.age;
     }
 
     @Override
-    public int getPickupDelay() {
+    public int getPickupDelayCM() {
         return this.pickupDelay;
     }
 
@@ -91,8 +91,8 @@ public abstract class ItemEntityMixin extends Entity implements ItemEntityInterf
             selfStack.increment(amount);
             self.setStack(selfStack);
 
-            this.pickupDelay = Math.max(((ItemEntityInterface)other).getPickupDelay(), this.pickupDelay);
-            this.age = Math.min(((ItemEntityInterface)other).getAge(), this.age);
+            this.pickupDelay = Math.max(((ItemEntityInterface)other).getPickupDelayCM(), this.pickupDelay);
+            this.age = Math.min(((ItemEntityInterface)other).getAgeCM(), this.age);
 
             otherStack.decrement(amount);
             if (otherStack.isEmpty())
diff --git a/src/main/java/carpet/mixins/LivingEntity_creativeFlyMixin.java b/src/main/java/carpet/mixins/LivingEntity_creativeFlyMixin.java
new file mode 100644
index 00000000..c50a364c
--- /dev/null
+++ b/src/main/java/carpet/mixins/LivingEntity_creativeFlyMixin.java
@@ -0,0 +1,50 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityType;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.world.World;
+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.Constant;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyConstant;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(LivingEntity.class)
+public abstract class LivingEntity_creativeFlyMixin extends Entity
+{
+    @Shadow public float flyingSpeed;
+
+    public LivingEntity_creativeFlyMixin(EntityType type, World world)
+    {
+        super(type, world);
+    }
+
+    @ModifyConstant(method = "travel", constant = @Constant(floatValue = 0.91F), expect = 2)
+    private float drag(float original)
+    {
+        if (CarpetSettings.creativeFlyDrag != 0.09 && (Object)this instanceof PlayerEntity)
+        {
+            PlayerEntity self = (PlayerEntity)(Object)(this);
+            if (self.abilities.flying && ! onGround )
+                return (float)(1.0-CarpetSettings.creativeFlyDrag);
+        }
+        return original;
+    }
+
+
+    @Inject(method = "getMovementSpeed(F)F", at = @At("HEAD"), cancellable = true)
+    private void flyingAltSpeed(float slipperiness, CallbackInfoReturnable cir)
+    {
+        if (CarpetSettings.creativeFlySpeed != 1.0D && (Object)this instanceof PlayerEntity)
+        {
+            PlayerEntity self = (PlayerEntity)(Object)(this);
+            if (self.abilities.flying && !onGround)
+                cir.setReturnValue(flyingSpeed* (float)CarpetSettings.creativeFlySpeed);
+        }
+    }
+}
diff --git a/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java b/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java
index 8ca20980..57eeaa7a 100644
--- a/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java
+++ b/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java
@@ -1,21 +1,33 @@
 package carpet.mixins;
 
+import carpet.CarpetSettings;
 import carpet.fakes.EntityInterface;
+import carpet.fakes.LivingEntityInterface;
 import carpet.script.EntityEventsGroup;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityType;
 import net.minecraft.entity.LivingEntity;
 import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.world.World;
 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.Constant;
 import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyConstant;
+import org.spongepowered.asm.mixin.injection.Redirect;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
 @Mixin(LivingEntity.class)
-public abstract class LivingEntity_scarpetEventsMixin extends Entity
+public abstract class LivingEntity_scarpetEventsMixin extends Entity implements LivingEntityInterface
 {
 
+    @Shadow protected abstract void jump();
+
+    @Shadow protected boolean jumping;
+
     public LivingEntity_scarpetEventsMixin(EntityType type, World world)
     {
         super(type, world);
@@ -26,4 +38,16 @@ public abstract class LivingEntity_scarpetEventsMixin extends Entity
     {
         ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.EntityEventType.ON_DEATH, this, damageSource_1.name);
     }
+
+    @Override
+    public void doJumpCM()
+    {
+        jump();
+    }
+
+    @Override
+    public boolean isJumpingCM()
+    {
+        return jumping;
+    }
 }
diff --git a/src/main/java/carpet/mixins/MinecraftClientMixin.java b/src/main/java/carpet/mixins/MinecraftClientMixin.java
index 2598239a..c2c9be06 100644
--- a/src/main/java/carpet/mixins/MinecraftClientMixin.java
+++ b/src/main/java/carpet/mixins/MinecraftClientMixin.java
@@ -1,7 +1,9 @@
 package carpet.mixins;
 
 import carpet.CarpetServer;
+import carpet.network.CarpetClient;
 import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
@@ -18,4 +20,10 @@ public class MinecraftClientMixin
         //CM start game hook
         CarpetServer.onGameStarted();
     }
+
+    @Inject(method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;)V", at = @At("HEAD"))
+    private void onCloseGame(Screen screen, CallbackInfo ci)
+    {
+        CarpetClient.disconnect();
+    }
 }
diff --git a/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java b/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java
index 5d5e330e..b11f9d8c 100644
--- a/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java
+++ b/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java
@@ -1,8 +1,10 @@
 package carpet.mixins;
 
 import carpet.CarpetServer;
+import carpet.CarpetSettings;
 import com.google.gson.JsonElement;
 import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.WorldGenerationProgressListener;
 import net.minecraft.world.level.LevelGeneratorType;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
@@ -41,4 +43,11 @@ public abstract class MinecraftServer_coreMixin
     {
         CarpetServer.onServerClosed((MinecraftServer) (Object) this);
     }
+
+    @Inject(method = "prepareStartRegion", at = @At("RETURN"))
+    private void afterSpawnCreated(WorldGenerationProgressListener worldGenerationProgressListener, CallbackInfo ci)
+    {
+        if (CarpetSettings.spawnChunksSize != 11)
+            CarpetSettings.ChangeSpawnChunksValidator.changeSpawnSize(CarpetSettings.spawnChunksSize);
+    }
 }
diff --git a/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java b/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java
index ac475142..9b695772 100644
--- a/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java
+++ b/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java
@@ -120,7 +120,7 @@ public abstract class MinecraftServer_tickspeedMixin extends ReentrantThreadExec
 
             this.profiler.startTick();
             this.profiler.push("tick");
-            this.tick(TickSpeed.time_warp_start_time != 0 ? ()->true : this::shouldKeepTicking);
+            this.tick(TickSpeed.time_warp_start_time != 0 ? ()->!hasRunningTasks() : this::shouldKeepTicking);
             this.profiler.swap("nextTickWait");
             this.field_19249 = true;
             this.field_19248 = Math.max(Util.getMeasuringTimeMs() + /*50L*/ msThisTick, this.timeReference);
diff --git a/src/main/java/carpet/mixins/PistonBlockEntity_creativeNoClipMixin.java b/src/main/java/carpet/mixins/PistonBlockEntity_creativeNoClipMixin.java
new file mode 100644
index 00000000..9e81659f
--- /dev/null
+++ b/src/main/java/carpet/mixins/PistonBlockEntity_creativeNoClipMixin.java
@@ -0,0 +1,29 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.block.entity.PistonBlockEntity;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.math.Direction;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(PistonBlockEntity.class)
+public class PistonBlockEntity_creativeNoClipMixin
+{
+    @Inject(method = "method_23672", at = @At("HEAD"), cancellable = true)
+    private static void dontPushSpectators(Direction direction, Entity entity, double d, Direction direction2, CallbackInfo ci)
+    {
+        if (CarpetSettings.creativeNoClip && entity instanceof PlayerEntity && (((PlayerEntity) entity).isCreative()) && ((PlayerEntity) entity).abilities.flying) ci.cancel();
+    }
+
+    @Redirect(method = "pushEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;setVelocity(DDD)V"))
+    private void ignoreAccel(Entity entity, double x, double y, double z)
+    {
+        if (CarpetSettings.creativeNoClip && entity instanceof PlayerEntity && (((PlayerEntity) entity).isCreative()) && ((PlayerEntity) entity).abilities.flying) return;
+        entity.setVelocity(x,y,z);
+    }
+}
diff --git a/src/main/java/carpet/mixins/PistonBlock_movableTEMixin.java b/src/main/java/carpet/mixins/PistonBlock_movableTEMixin.java
index a17f5dff..0df4236a 100644
--- a/src/main/java/carpet/mixins/PistonBlock_movableTEMixin.java
+++ b/src/main/java/carpet/mixins/PistonBlock_movableTEMixin.java
@@ -5,6 +5,7 @@ import carpet.fakes.PistonBlockEntityInterface;
 import com.google.common.collect.Lists;
 import net.minecraft.block.*;
 import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.piston.PistonBehavior;
 import net.minecraft.block.piston.PistonHandler;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Direction;
@@ -62,6 +63,17 @@ public abstract class PistonBlock_movableTEMixin extends FacingBlock
             return !(CarpetSettings.movableBlockEntities && isPushableBlockEntity(block));
         }
     }
+
+    @Redirect(method = "isMovable", at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/block/BlockState;getPistonBehavior()Lnet/minecraft/block/piston/PistonBehavior;"
+    ))
+    private static PistonBehavior moveGrindstones(BlockState blockState)
+    {
+        if (CarpetSettings.movableBlockEntities && blockState.getBlock() == Blocks.GRINDSTONE) return PistonBehavior.NORMAL;
+        return blockState.getPistonBehavior();
+    }
+
     @Inject(method = "move", at = @At(value = "INVOKE", shift = At.Shift.BEFORE,
             target = "Ljava/util/List;size()I", ordinal = 4),locals = LocalCapture.CAPTURE_FAILHARD)
     private void onMove(World world_1, BlockPos blockPos_1, Direction direction_1, boolean boolean_1,
diff --git a/src/main/java/carpet/mixins/PlayerEntity_antiCheatDisabled.java b/src/main/java/carpet/mixins/PlayerEntity_antiCheatDisabled.java
index 2f211d34..e8440bb4 100644
--- a/src/main/java/carpet/mixins/PlayerEntity_antiCheatDisabled.java
+++ b/src/main/java/carpet/mixins/PlayerEntity_antiCheatDisabled.java
@@ -23,7 +23,7 @@ public abstract class PlayerEntity_antiCheatDisabled
     @Inject(method = "method_23668", at = @At("HEAD"), cancellable = true)
     private void allowDeploys(CallbackInfoReturnable cir)
     {
-        if (CarpetSettings.antiCheatDisabled && CarpetServer.minecraft_server.isDedicated())
+        if (CarpetSettings.antiCheatDisabled && CarpetServer.minecraft_server != null && CarpetServer.minecraft_server.isDedicated())
         {
             ItemStack itemStack_1 = getEquippedStack(EquipmentSlot.CHEST);
             if (itemStack_1.getItem() == Items.ELYTRA && ElytraItem.isUsable(itemStack_1)) {
diff --git a/src/main/java/carpet/mixins/PlayerEntity_creativeNoClipMixin.java b/src/main/java/carpet/mixins/PlayerEntity_creativeNoClipMixin.java
new file mode 100644
index 00000000..ea50dab4
--- /dev/null
+++ b/src/main/java/carpet/mixins/PlayerEntity_creativeNoClipMixin.java
@@ -0,0 +1,56 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.entity.EntityType;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.MovementType;
+import net.minecraft.entity.player.PlayerAbilities;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+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.Redirect;
+
+@Mixin(PlayerEntity.class)
+public abstract class PlayerEntity_creativeNoClipMixin extends LivingEntity
+{
+    @Shadow public abstract boolean isCreative();
+
+    @Shadow @Final public PlayerAbilities abilities;
+
+    protected PlayerEntity_creativeNoClipMixin(EntityType type, World world)
+    {
+        super(type, world);
+    }
+
+    @Redirect(method = "tick", at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/entity/player/PlayerEntity;isSpectator()Z")
+    )
+    private boolean canClipTroughWorld(PlayerEntity playerEntity)
+    {
+        return playerEntity.isSpectator() || (CarpetSettings.creativeNoClip && playerEntity.isCreative() && playerEntity.abilities.flying);
+
+    }
+
+    @Redirect(method = "tickMovement", at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/entity/player/PlayerEntity;isSpectator()Z")
+    )
+    private boolean collidesWithEntities(PlayerEntity playerEntity)
+    {
+        return playerEntity.isSpectator() || (CarpetSettings.creativeNoClip && playerEntity.isCreative() && playerEntity.abilities.flying);
+    }
+
+    @Redirect(method = "updateSize", at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/entity/player/PlayerEntity;isSpectator()Z")
+    )
+    private boolean spectatorsDontPose(PlayerEntity playerEntity)
+    {
+        return playerEntity.isSpectator() || (CarpetSettings.creativeNoClip && playerEntity.isCreative() && playerEntity.abilities.flying);
+    }
+}
diff --git a/src/main/java/carpet/mixins/PlayerManager_coreMixin.java b/src/main/java/carpet/mixins/PlayerManager_coreMixin.java
index ecd2fe61..62eeefff 100644
--- a/src/main/java/carpet/mixins/PlayerManager_coreMixin.java
+++ b/src/main/java/carpet/mixins/PlayerManager_coreMixin.java
@@ -9,6 +9,8 @@ import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
+import static carpet.script.CarpetEventServer.Event.PLAYER_CONNECTS;
+
 @Mixin(PlayerManager.class)
 public class PlayerManager_coreMixin
 {
@@ -16,5 +18,6 @@ public class PlayerManager_coreMixin
     private void onPlayerConnected(ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci)
     {
         CarpetServer.onPlayerLoggedIn(player);
+        PLAYER_CONNECTS.onPlayerEvent(player);
     }
 }
diff --git a/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java b/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java
new file mode 100644
index 00000000..9b03218d
--- /dev/null
+++ b/src/main/java/carpet/mixins/PlayerManager_scarpetEventsMixin.java
@@ -0,0 +1,21 @@
+package carpet.mixins;
+
+import net.minecraft.server.PlayerManager;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.world.dimension.DimensionType;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import static carpet.script.CarpetEventServer.Event.PLAYER_RESPAWNS;
+
+@Mixin(PlayerManager.class)
+public class PlayerManager_scarpetEventsMixin
+{
+    @Inject(method = "respawnPlayer", at = @At("HEAD"))
+    private void onRespawn(ServerPlayerEntity player, DimensionType dimension, boolean alive, CallbackInfoReturnable cir)
+    {
+        PLAYER_RESPAWNS.onPlayerEvent(player);
+    }
+}
diff --git a/src/main/java/carpet/mixins/ServerChunkManagerMixin.java b/src/main/java/carpet/mixins/ServerChunkManagerMixin.java
index 109c9bb5..f0fab105 100644
--- a/src/main/java/carpet/mixins/ServerChunkManagerMixin.java
+++ b/src/main/java/carpet/mixins/ServerChunkManagerMixin.java
@@ -1,8 +1,10 @@
 package carpet.mixins;
 
+import carpet.fakes.ServerChunkManagerInterface;
 import carpet.utils.SpawnReporter;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import net.minecraft.entity.EntityCategory;
+import net.minecraft.server.world.ChunkTicketManager;
 import net.minecraft.server.world.ServerChunkManager;
 import net.minecraft.server.world.ServerWorld;
 import net.minecraft.util.math.BlockPos;
@@ -24,12 +26,20 @@ import java.util.HashMap;
 import java.util.HashSet;
 
 @Mixin(ServerChunkManager.class)
-public abstract class ServerChunkManagerMixin
+public abstract class ServerChunkManagerMixin implements ServerChunkManagerInterface
 {
     @Shadow @Final private ServerWorld world;
 
     @Shadow @Final private static int CHUNKS_ELIGIBLE_FOR_SPAWNING;
 
+    @Shadow @Final private ChunkTicketManager ticketManager;
+
+    @Override // shared between scarpet and spawnChunks setting
+    public ChunkTicketManager getCMTicketManager()
+    {
+        return ticketManager;
+    }
+
     @Inject(
             method = "tickChunks",
             locals = LocalCapture.CAPTURE_FAILHARD,
diff --git a/src/main/java/carpet/mixins/ServerChunkManager_scarpetMixin.java b/src/main/java/carpet/mixins/ServerChunkManager_scarpetMixin.java
deleted file mode 100644
index 9cec83e8..00000000
--- a/src/main/java/carpet/mixins/ServerChunkManager_scarpetMixin.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package carpet.mixins;
-
-import net.minecraft.server.world.ChunkTicketManager;
-import net.minecraft.server.world.ServerChunkManager;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.gen.Accessor;
-
-@Mixin(ServerChunkManager.class)
-public interface ServerChunkManager_scarpetMixin
-{
-    @Accessor
-    ChunkTicketManager getTicketManager();
-}
diff --git a/src/main/java/carpet/mixins/ServerPlayNetworkHandlerMixin.java b/src/main/java/carpet/mixins/ServerPlayNetworkHandlerMixin.java
new file mode 100644
index 00000000..bec53d32
--- /dev/null
+++ b/src/main/java/carpet/mixins/ServerPlayNetworkHandlerMixin.java
@@ -0,0 +1,31 @@
+package carpet.mixins;
+
+import carpet.fakes.CustomPayloadC2SPacketInterface;
+import carpet.network.CarpetClient;
+import carpet.network.ServerNetworkHandler;
+import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket;
+import net.minecraft.server.network.ServerPlayNetworkHandler;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.Identifier;
+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;
+
+@Mixin(ServerPlayNetworkHandler.class)
+public class ServerPlayNetworkHandlerMixin
+{
+    @Shadow public ServerPlayerEntity player;
+
+    @Inject(method = "onCustomPayload", at = @At("HEAD"), cancellable = true)
+    private void onCustomCarpetPayload(CustomPayloadC2SPacket packet, CallbackInfo ci)
+    {
+        Identifier channel = ((CustomPayloadC2SPacketInterface) packet).getPacketChannel();
+        if (CarpetClient.CARPET_CHANNEL.equals(channel))
+        {
+            ServerNetworkHandler.handleData(((CustomPayloadC2SPacketInterface) packet).getPacketData(), player);
+            ci.cancel();
+        }
+    }
+}
diff --git a/src/main/java/carpet/mixins/ServerPlayNetworkHandler_coreMixin.java b/src/main/java/carpet/mixins/ServerPlayNetworkHandler_coreMixin.java
index 1dfbc237..43fabd5c 100644
--- a/src/main/java/carpet/mixins/ServerPlayNetworkHandler_coreMixin.java
+++ b/src/main/java/carpet/mixins/ServerPlayNetworkHandler_coreMixin.java
@@ -13,6 +13,8 @@ import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
+import static carpet.script.CarpetEventServer.Event.PLAYER_DISCONNECTS;
+
 @Mixin(ServerPlayNetworkHandler.class)
 public class ServerPlayNetworkHandler_coreMixin {
     @Shadow
@@ -21,5 +23,6 @@ public class ServerPlayNetworkHandler_coreMixin {
     @Inject(method = "onDisconnected", at = @At("HEAD"))
     private void onPlayerDisconnect(Text reason, CallbackInfo ci) {
         CarpetServer.onPlayerLoggedOut(this.player);
+        if (PLAYER_DISCONNECTS.isNeeded()) PLAYER_DISCONNECTS.onPlayerMessage(player, reason.asString());
     }
 }
diff --git a/src/main/java/carpet/mixins/ServerPlayNetworkHandler_tickMixin.java b/src/main/java/carpet/mixins/ServerPlayNetworkHandler_tickMixin.java
index 7b6ec9f2..919fc3ad 100644
--- a/src/main/java/carpet/mixins/ServerPlayNetworkHandler_tickMixin.java
+++ b/src/main/java/carpet/mixins/ServerPlayNetworkHandler_tickMixin.java
@@ -31,6 +31,10 @@ public class ServerPlayNetworkHandler_tickMixin
         }
     }
 
+    // to skip reposition adjustment check
+    private static long lastMovedTick = 0L;
+    private static double lastMoved = 0.0D;
+
     @Inject(method = "onPlayerMove",  at = @At(
             value = "INVOKE",
             target = "Lnet/minecraft/server/network/ServerPlayerEntity;isSleeping()Z",
@@ -38,10 +42,23 @@ public class ServerPlayNetworkHandler_tickMixin
     ))
     private void checkMove(PlayerMoveC2SPacket p, CallbackInfo ci)
     {
-        if (Math.abs(p.getX(player.getX()) - lastTickX) > 0.0001D
-                || Math.abs(p.getY(player.getY()) - lastTickY) > 0.0001D
-                || Math.abs(p.getY(player.getZ()) - lastTickZ) > 0.0001D)
+        double movedBy = player.getPos().squaredDistanceTo(lastTickX, lastTickY, lastTickZ);
+        if (movedBy == 0.0D) return;
+        // corrective tick
+        if (movedBy < 0.0009 && lastMoved > 0.0009 && Math.abs(player.getServer().getTicks()-lastMovedTick-20)<2)
         {
+            //CarpetSettings.LOG.error("Corrective movement packet");
+            return;
+        }
+        if (movedBy > 0.0D)
+        {
+            //CarpetSettings.LOG.error(String.format(
+            //        "moved by %.6f at %d",
+            //        player.getPos().squaredDistanceTo(lastTickX, lastTickY, lastTickZ),
+            //        player.getServer().getTicks()-lastMovedTick
+            //));
+            lastMoved = movedBy;
+            lastMovedTick = player.getServer().getTicks();
             TickSpeed.reset_player_active_timeout();
         }
     }
diff --git a/src/main/java/carpet/mixins/ServerPlayerEntity_scarpetEventMixin.java b/src/main/java/carpet/mixins/ServerPlayerEntity_scarpetEventMixin.java
index c5beb8da..51ddea2f 100644
--- a/src/main/java/carpet/mixins/ServerPlayerEntity_scarpetEventMixin.java
+++ b/src/main/java/carpet/mixins/ServerPlayerEntity_scarpetEventMixin.java
@@ -3,20 +3,26 @@ package carpet.mixins;
 import carpet.fakes.EntityInterface;
 import carpet.script.EntityEventsGroup;
 import com.mojang.authlib.GameProfile;
+import net.minecraft.entity.Entity;
 import net.minecraft.entity.damage.DamageSource;
 import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
 import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.stat.Stat;
 import net.minecraft.util.Hand;
+import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
+import net.minecraft.world.dimension.DimensionType;
 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.Redirect;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
+import static carpet.script.CarpetEventServer.Event.PLAYER_CHANGES_DIMENSION;
+import static carpet.script.CarpetEventServer.Event.PLAYER_DIES;
 import static carpet.script.CarpetEventServer.Event.PLAYER_FINISHED_USING_ITEM;
 import static carpet.script.CarpetEventServer.Event.STATISTICS;
 
@@ -30,6 +36,8 @@ public abstract class ServerPlayerEntity_scarpetEventMixin extends PlayerEntity
 
     @Shadow protected abstract void consumeItem();
 
+    @Shadow public boolean notInAnyWorld;
+
     @Redirect(method = "consumeItem", at = @At(
             value = "INVOKE",
             target = "Lnet/minecraft/entity/player/PlayerEntity;consumeItem()V"
@@ -61,6 +69,10 @@ public abstract class ServerPlayerEntity_scarpetEventMixin extends PlayerEntity
     private void onDeathEvent(DamageSource source, CallbackInfo ci)
     {
         ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.EntityEventType.ON_DEATH, this, source.name);
+        if (PLAYER_DIES.isNeeded())
+        {
+            PLAYER_DIES.onPlayerEvent((ServerPlayerEntity) (Object)this);
+        }
     }
 
     @Redirect(method = "method_14218", at = @At(
@@ -72,4 +84,30 @@ public abstract class ServerPlayerEntity_scarpetEventMixin extends PlayerEntity
         if (!((EntityInterface)serverPlayerEntity.getVehicle()).isPermanentVehicle()) // won't since that method makes sure its not null
             serverPlayerEntity.setSneaking(sneaking);
     }
+
+    private Vec3d previousLocation;
+    private DimensionType previousDimension;
+
+    @Inject(method = "changeDimension", at = @At("HEAD"))
+    private void logPreviousCoordinates(DimensionType newDimension, CallbackInfoReturnable cir)
+    {
+        previousLocation = getPos();
+        previousDimension = dimension;
+    }
+
+    @Inject(method = "changeDimension", at = @At("RETURN"))
+    private void atChangeDimension(DimensionType newDimension, CallbackInfoReturnable cir)
+    {
+        if (PLAYER_CHANGES_DIMENSION.isNeeded())
+        {
+            ServerPlayerEntity player = (ServerPlayerEntity) (Object)this;
+            Vec3d to = null;
+            if (!notInAnyWorld || previousDimension != DimensionType.THE_END || newDimension != DimensionType.OVERWORLD)
+            {
+                to = getPos();
+            }
+            PLAYER_CHANGES_DIMENSION.onDimensionChange(player, previousLocation, to, previousDimension, newDimension);
+        }
+    }
+
 }
diff --git a/src/main/java/carpet/mixins/ServerWorld_spawnChunksMixin.java b/src/main/java/carpet/mixins/ServerWorld_spawnChunksMixin.java
new file mode 100644
index 00000000..a462a6f6
--- /dev/null
+++ b/src/main/java/carpet/mixins/ServerWorld_spawnChunksMixin.java
@@ -0,0 +1,17 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.server.world.ServerWorld;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.Constant;
+import org.spongepowered.asm.mixin.injection.ModifyConstant;
+
+@Mixin(ServerWorld.class)
+public class ServerWorld_spawnChunksMixin
+{
+    @ModifyConstant(method = "setSpawnPos", constant = @Constant(intValue = 11), expect = 2)
+    private int pushLimit(int original)
+    {
+        return CarpetSettings.spawnChunksSize;
+    }
+}
diff --git a/src/main/java/carpet/mixins/StructurePiece_scarpetPlopMixin.java b/src/main/java/carpet/mixins/StructurePiece_scarpetPlopMixin.java
new file mode 100644
index 00000000..94b2cfc1
--- /dev/null
+++ b/src/main/java/carpet/mixins/StructurePiece_scarpetPlopMixin.java
@@ -0,0 +1,22 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.structure.StructurePiece;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.chunk.Chunk;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(StructurePiece.class)
+public class StructurePiece_scarpetPlopMixin
+{
+    @Redirect(method = "addBlock", at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/world/chunk/Chunk;markBlockForPostProcessing(Lnet/minecraft/util/math/BlockPos;)V"
+    ))
+    private void markOrNot(Chunk chunk, BlockPos pos)
+    {
+        if (!CarpetSettings.skipGenerationChecks) chunk.markBlockForPostProcessing(pos);
+    }
+}
diff --git a/src/main/java/carpet/mixins/TntEntityMixin.java b/src/main/java/carpet/mixins/TntEntityMixin.java
index 4a296389..2e67b1e5 100644
--- a/src/main/java/carpet/mixins/TntEntityMixin.java
+++ b/src/main/java/carpet/mixins/TntEntityMixin.java
@@ -72,7 +72,7 @@ public abstract class TntEntityMixin extends Entity implements TntEntityInterfac
     private void onExplode(CallbackInfo ci)
     {
         if (LoggerRegistry.__tnt && logHelper != null)
-            logHelper.onExploded(getX(), getY(), getZ());
+            logHelper.onExploded(getX(), getY(), getZ(), this.world.getTime());
 
         if (mergedTNT > 1)
             for (int i = 0; i < mergedTNT - 1; i++)
diff --git a/src/main/java/carpet/mixins/WorldRenderer_creativeNoClipMixin.java b/src/main/java/carpet/mixins/WorldRenderer_creativeNoClipMixin.java
new file mode 100644
index 00000000..4e8a8109
--- /dev/null
+++ b/src/main/java/carpet/mixins/WorldRenderer_creativeNoClipMixin.java
@@ -0,0 +1,19 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.render.WorldRenderer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(WorldRenderer.class)
+public class WorldRenderer_creativeNoClipMixin
+{
+    @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;isSpectator()Z"))
+    private boolean canSeeWorld(ClientPlayerEntity clientPlayerEntity)
+    {
+        return clientPlayerEntity.isSpectator() || (CarpetSettings.creativeNoClip && clientPlayerEntity.isCreative());
+    }
+
+}
diff --git a/src/main/java/carpet/mixins/WorldRenderer_fogOffMixin.java b/src/main/java/carpet/mixins/WorldRenderer_fogOffMixin.java
new file mode 100644
index 00000000..a620c194
--- /dev/null
+++ b/src/main/java/carpet/mixins/WorldRenderer_fogOffMixin.java
@@ -0,0 +1,23 @@
+package carpet.mixins;
+
+import carpet.CarpetSettings;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.world.dimension.Dimension;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(value = WorldRenderer.class, priority = 69420)
+public class WorldRenderer_fogOffMixin
+{
+    @Redirect(method = "render", require = 0, expect = 0, at = @At(
+            value = "INVOKE",
+            target = "Lnet/minecraft/world/dimension/Dimension;isFogThick(II)Z"
+    ))
+    private boolean isReallyThick(Dimension dimension, int x, int z)
+    {
+        if (CarpetSettings.fogOff) return false;
+        return dimension.isFogThick(x, z);
+    }
+
+}
diff --git a/src/main/java/carpet/mixins/WorldRenderer_scarpetRenderMixin.java b/src/main/java/carpet/mixins/WorldRenderer_scarpetRenderMixin.java
new file mode 100644
index 00000000..df777e69
--- /dev/null
+++ b/src/main/java/carpet/mixins/WorldRenderer_scarpetRenderMixin.java
@@ -0,0 +1,32 @@
+package carpet.mixins;
+
+import carpet.network.CarpetClient;
+import carpet.script.utils.ShapesRenderer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilderStorage;
+import net.minecraft.client.render.Camera;
+import net.minecraft.client.render.GameRenderer;
+import net.minecraft.client.render.LightmapTextureManager;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.client.util.math.Matrix4f;
+import net.minecraft.client.util.math.MatrixStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(WorldRenderer.class)
+public class WorldRenderer_scarpetRenderMixin
+{
+    @Inject(method = "", at = @At("RETURN"))
+    private void addRenderers(MinecraftClient client, BufferBuilderStorage bufferBuilders, CallbackInfo ci)
+    {
+        CarpetClient.shapes = new ShapesRenderer(client);
+    }
+
+    @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/debug/DebugRenderer;render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;DDD)V"))
+    private void renderScarpetThings(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f matrix4f, CallbackInfo ci)
+    {
+        if (CarpetClient.shapes != null) CarpetClient.shapes.render(camera, tickDelta);
+    }
+}
diff --git a/src/main/java/carpet/network/CarpetClient.java b/src/main/java/carpet/network/CarpetClient.java
new file mode 100644
index 00000000..7010eb31
--- /dev/null
+++ b/src/main/java/carpet/network/CarpetClient.java
@@ -0,0 +1,54 @@
+package carpet.network;
+
+import carpet.CarpetServer;
+import carpet.script.utils.ShapesRenderer;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.util.Identifier;
+
+public class CarpetClient
+{
+    public static final Object sync = new Object();
+    public static final int HI = 69;
+    public static final int HELLO = 420;
+    public static final int DATA = 1;
+    public static ShapesRenderer shapes = null;
+
+    private static ClientPlayerEntity clientPlayer = null;
+    private static boolean isServerCarpet = false;
+    public static String serverCarpetVersion;
+    public static final Identifier CARPET_CHANNEL = new Identifier("carpet:hello");
+
+    public static void gameJoined(ClientPlayerEntity player)
+    {
+        synchronized (sync)
+        {
+            clientPlayer = player;
+            // client didn't say hi back yet
+            if (isServerCarpet)
+                ClientNetworkHandler.respondHello();
+
+        }
+    }
+
+    public static void disconnect()
+    {
+        isServerCarpet = false;
+        clientPlayer = null;
+        CarpetServer.disconnect();
+    }
+
+    public static void setCarpet()
+    {
+        isServerCarpet = true;
+    }
+
+    public static ClientPlayerEntity getPlayer()
+    {
+        return clientPlayer;
+    }
+
+    public static boolean isCarpet()
+    {
+        return isServerCarpet;
+    }
+}
diff --git a/src/main/java/carpet/network/ClientNetworkHandler.java b/src/main/java/carpet/network/ClientNetworkHandler.java
new file mode 100644
index 00000000..c65e2ac6
--- /dev/null
+++ b/src/main/java/carpet/network/ClientNetworkHandler.java
@@ -0,0 +1,102 @@
+package carpet.network;
+
+import carpet.CarpetServer;
+import carpet.CarpetSettings;
+import carpet.helpers.TickSpeed;
+import carpet.settings.ParsedRule;
+import io.netty.buffer.Unpooled;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.nbt.AbstractNumberTag;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket;
+import net.minecraft.util.PacketByteBuf;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+
+public class ClientNetworkHandler
+{
+    private static Map> dataHandlers = new HashMap>(){{
+        put("Rules", (p, t) -> {
+            CompoundTag ruleset = (CompoundTag)t;
+            for (String ruleName: ruleset.getKeys())
+            {
+                ParsedRule rule = CarpetServer.settingsManager.getRule(ruleName);
+                if (rule == null)
+                    CarpetSettings.LOG.error("Received unknown rule: "+ruleName);
+                else
+                {
+                    CompoundTag ruleNBT = (CompoundTag) ruleset.get(ruleName);
+                    String value = ruleNBT.getString("Value");
+                    rule.set(null, value);
+                }
+            }
+        });
+        put("TickRate", (p, t) -> TickSpeed.tickrate(((AbstractNumberTag)t).getFloat(), false));
+        put("scShape", (p, t) -> { // deprecated
+            if (CarpetClient.shapes != null)
+                CarpetClient.shapes.addShape((CompoundTag)t);
+        });
+        put("scShapes", (p, t) -> {
+            if (CarpetClient.shapes != null)
+                CarpetClient.shapes.addShapes((ListTag) t);
+        });
+    }};
+
+
+    public static void handleData(PacketByteBuf data, ClientPlayerEntity player)
+    {
+        if (data != null)
+        {
+            int id = data.readVarInt();
+            if (id == CarpetClient.HI)
+                onHi(data);
+            if (id == CarpetClient.DATA)
+                onSyncData(data, player);
+        }
+    }
+
+    private static void onHi(PacketByteBuf data)
+    {
+        synchronized (CarpetClient.sync)
+        {
+            CarpetClient.setCarpet();
+            CarpetClient.serverCarpetVersion = data.readString(64);
+            if (CarpetSettings.carpetVersion.equals(CarpetClient.serverCarpetVersion))
+            {
+                CarpetSettings.LOG.info("Joined carpet server with matching carpet version");
+            }
+            else
+            {
+                CarpetSettings.LOG.warn("Joined carpet server with another carpet version: "+CarpetClient.serverCarpetVersion);
+            }
+            if (CarpetClient.getPlayer() != null)
+                respondHello();
+
+        }
+    }
+
+    public static void respondHello()
+    {
+        CarpetClient.getPlayer().networkHandler.sendPacket(new CustomPayloadC2SPacket(
+                CarpetClient.CARPET_CHANNEL,
+                (new PacketByteBuf(Unpooled.buffer())).writeVarInt(CarpetClient.HELLO).writeString(CarpetSettings.carpetVersion)
+        ));
+    }
+
+    private static void onSyncData(PacketByteBuf data, ClientPlayerEntity player)
+    {
+        CompoundTag compound = data.readCompoundTag();
+        if (compound == null) return;
+        for (String key: compound.getKeys())
+        {
+            if (dataHandlers.containsKey(key))
+                dataHandlers.get(key).accept(player, compound.get(key));
+            else
+                CarpetSettings.LOG.error("Unknown carpet data: "+key);
+        }
+    }
+}
diff --git a/src/main/java/carpet/network/ServerNetworkHandler.java b/src/main/java/carpet/network/ServerNetworkHandler.java
new file mode 100644
index 00000000..6de0b77b
--- /dev/null
+++ b/src/main/java/carpet/network/ServerNetworkHandler.java
@@ -0,0 +1,178 @@
+package carpet.network;
+
+import carpet.CarpetServer;
+import carpet.CarpetSettings;
+import carpet.helpers.TickSpeed;
+import carpet.settings.ParsedRule;
+import io.netty.buffer.Unpooled;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.PacketByteBuf;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class ServerNetworkHandler
+{
+    private static Map remoteCarpetPlayers = new HashMap<>();
+    private static Set validCarpetPlayers = new HashSet<>();
+
+    public static void handleData(PacketByteBuf data, ServerPlayerEntity player)
+    {
+        if (data != null)
+        {
+            int id = data.readVarInt();
+            if (id == CarpetClient.HELLO)
+                onHello(player, data);
+        }
+    }
+    public static void onPlayerJoin(ServerPlayerEntity playerEntity)
+    {
+        if (!playerEntity.networkHandler.connection.isLocal())
+        {
+            playerEntity.networkHandler.sendPacket(new CustomPayloadS2CPacket(
+                    CarpetClient.CARPET_CHANNEL,
+                    (new PacketByteBuf(Unpooled.buffer())).writeVarInt(CarpetClient.HI).writeString(CarpetSettings.carpetVersion)
+            ));
+        }
+        else
+        {
+            validCarpetPlayers.add(playerEntity);
+        }
+
+    }
+
+    public static void onHello(ServerPlayerEntity playerEntity, PacketByteBuf packetData)
+    {
+
+        validCarpetPlayers.add(playerEntity);
+        String clientVersion = packetData.readString(64);
+        remoteCarpetPlayers.put(playerEntity, clientVersion);
+        if (clientVersion.equals(CarpetSettings.carpetVersion))
+            CarpetSettings.LOG.info("Player "+playerEntity.getName().getString()+" joined with a matching carpet client");
+        else
+            CarpetSettings.LOG.warn("Player "+playerEntity.getName().getString()+" joined with another carpet version: "+clientVersion);
+        DataBuilder data = DataBuilder.create().withTickRate();
+        CarpetServer.settingsManager.getRules().forEach(data::withRule);
+        playerEntity.networkHandler.sendPacket(new CustomPayloadS2CPacket(CarpetClient.CARPET_CHANNEL, data.build() ));
+    }
+    
+    public static void updateRuleWithConnectedClients(ParsedRule rule)
+    {
+        if (CarpetSettings.superSecretSetting) return;
+        for (ServerPlayerEntity player : remoteCarpetPlayers.keySet())
+        {
+            player.networkHandler.sendPacket(new CustomPayloadS2CPacket(
+                    CarpetClient.CARPET_CHANNEL,
+                    DataBuilder.create().withRule(rule).build()
+            ));
+        }
+    }
+    
+    public static void updateTickSpeedToConnectedPlayers()
+    {
+        if (CarpetSettings.superSecretSetting) return;
+        for (ServerPlayerEntity player : remoteCarpetPlayers.keySet())
+        {
+            player.networkHandler.sendPacket(new CustomPayloadS2CPacket(
+                    CarpetClient.CARPET_CHANNEL,
+                    DataBuilder.create().withTickRate().build()
+            ));
+        }
+    }
+
+    public static void broadcastCustomCommand(String command, Tag data)
+    {
+        if (CarpetSettings.superSecretSetting) return;
+        for (ServerPlayerEntity player : validCarpetPlayers)
+        {
+            player.networkHandler.sendPacket(new CustomPayloadS2CPacket(
+                    CarpetClient.CARPET_CHANNEL,
+                    DataBuilder.create().withCustomNbt(command, data).build()
+            ));
+        }
+    }
+
+    public static void sendCustomCommand(ServerPlayerEntity player, String command, Tag data)
+    {
+        if (isValidCarpetPlayer(player))
+        {
+            player.networkHandler.sendPacket(new CustomPayloadS2CPacket(
+                    CarpetClient.CARPET_CHANNEL,
+                    DataBuilder.create().withCustomNbt(command, data).build()
+            ));
+        }
+    }
+
+
+    public static void onPlayerLoggedOut(ServerPlayerEntity player)
+    {
+        validCarpetPlayers.remove(player);
+        if (!player.networkHandler.connection.isLocal())
+            remoteCarpetPlayers.remove(player);
+    }
+
+    public static void close()
+    {
+        remoteCarpetPlayers.clear();
+        validCarpetPlayers.clear();
+    }
+
+    public static boolean isValidCarpetPlayer(ServerPlayerEntity player)
+    {
+        if (CarpetSettings.superSecretSetting) return false;
+        return validCarpetPlayers.contains(player);
+
+    }
+
+    private static class DataBuilder
+    {
+        private CompoundTag tag;
+        private static DataBuilder create()
+        {
+            return new DataBuilder();
+        }
+        private DataBuilder()
+        {
+            tag = new CompoundTag();
+        }
+        private DataBuilder withTickRate()
+        {
+            tag.putFloat("TickRate", TickSpeed.tickrate);
+            return this;
+        }
+        private DataBuilder withRule(ParsedRule rule)
+        {
+            CompoundTag rules = (CompoundTag) tag.get("Rules");
+            if (rules == null)
+            {
+                rules = new CompoundTag();
+                tag.put("Rules", rules);
+            }
+            CompoundTag ruleNBT = new CompoundTag();
+            ruleNBT.putString("Value", rule.getAsString());
+            rules.put(rule.name, ruleNBT);
+            return this;
+        }
+
+        public DataBuilder withCustomNbt(String key, Tag value)
+        {
+            tag.put(key, value);
+            return this;
+        }
+
+        private PacketByteBuf build()
+        {
+            PacketByteBuf packetBuf = new PacketByteBuf(Unpooled.buffer());
+            packetBuf.writeVarInt(CarpetClient.DATA);
+            packetBuf.writeCompoundTag(tag);
+            return packetBuf;
+        }
+
+
+    }
+}
diff --git a/src/main/java/carpet/script/CarpetEventServer.java b/src/main/java/carpet/script/CarpetEventServer.java
index e6c87413..2eafcd6d 100644
--- a/src/main/java/carpet/script/CarpetEventServer.java
+++ b/src/main/java/carpet/script/CarpetEventServer.java
@@ -12,6 +12,8 @@ import carpet.script.value.StringValue;
 import carpet.script.value.Value;
 import carpet.utils.Messenger;
 import net.minecraft.block.BlockState;
+import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.entity.Entity;
 import net.minecraft.server.network.ServerPlayerEntity;
@@ -50,6 +52,7 @@ import java.util.stream.Collectors;
 public class CarpetEventServer
 {
     public final List scheduledCalls = new LinkedList<>();
+    public final MinecraftServer server;
 
     public static class Callback
     {
@@ -125,7 +128,13 @@ public class CarpetEventServer
                 List argv = argumentSupplier.get(); // empty for onTickDone
                 ServerCommandSource source = cmdSourceSupplier.get();
                 assert argv.size() == reqArgs;
-                callList.removeIf(call -> !CarpetServer.scriptServer.runas(source, call.host, call.function, argv)); // this actually does the calls
+                List fails = new ArrayList<>();
+                for (Callback call: callList)
+                {
+                    if (!CarpetServer.scriptServer.runas(source, call.host, call.function, argv))
+                        fails.add(call);
+                }
+                for (Callback call : fails) callList.remove(call);
             }
         }
         public boolean addEventCall(ServerCommandSource source,  String hostName, String funName, Function verifier)
@@ -479,6 +488,90 @@ public class CarpetEventServer
                 handler.call( () -> Collections.singletonList(((c, t) -> new EntityValue(player))), player::getCommandSource);
             }
         },
+        PLAYER_TAKES_DAMAGE("player_takes_damage", 4, false)
+        {
+            @Override
+            public void onDamage(Entity target, float amount, DamageSource source)
+            {
+                handler.call( () ->
+                {
+                    return Arrays.asList(
+                            ((c, t) -> new EntityValue(target)),
+                            ((c, t) -> new NumericValue(amount)),
+                            ((c, t) ->new StringValue(source.getName())),
+                            ((c, t) -> source.getAttacker()==null?Value.NULL:new EntityValue(source.getAttacker()))
+                    );
+                }, target::getCommandSource);
+            }
+        },
+        PLAYER_DEALS_DAMAGE("player_deals_damage", 3, false)
+        {
+            @Override
+            public void onDamage(Entity target, float amount, DamageSource source)
+            {
+                handler.call( () ->
+                {
+                    return Arrays.asList(
+                            ((c, t) -> new EntityValue(source.getAttacker())),
+                            ((c, t) -> new NumericValue(amount)),
+                            ((c, t) -> new EntityValue(target))
+                    );
+                }, () -> source.getAttacker().getCommandSource());
+            }
+        },
+        PLAYER_DIES("player_dies", 1, false)
+        {
+            @Override
+            public void onPlayerEvent(ServerPlayerEntity player)
+            {
+                handler.call( () -> Collections.singletonList(((c, t) -> new EntityValue(player))), player::getCommandSource);
+            }
+        },
+        PLAYER_RESPAWNS("player_respawns", 1, false)
+        {
+            @Override
+            public void onPlayerEvent(ServerPlayerEntity player)
+            {
+                handler.call( () -> Collections.singletonList(((c, t) -> new EntityValue(player))), player::getCommandSource);
+            }
+        },
+        PLAYER_CHANGES_DIMENSION("player_changes_dimension", 5, false)
+        {
+            @Override
+            public void onDimensionChange(ServerPlayerEntity player, Vec3d from, Vec3d to, DimensionType fromDim, DimensionType dimTo)
+            {
+                // eligibility already checked in mixin
+                Value fromValue = ListValue.fromTriple(from.x, from.y, from.z);
+                Value toValue = (to == null)?Value.NULL:ListValue.fromTriple(to.x, to.y, to.z);
+                Value fromDimStr = new StringValue(NBTSerializableValue.nameFromRegistryId(Registry.DIMENSION_TYPE.getId(fromDim)));
+                Value toDimStr = new StringValue(NBTSerializableValue.nameFromRegistryId(Registry.DIMENSION_TYPE.getId(dimTo)));
+
+                handler.call( () -> Arrays.asList(
+                        ((c, t) -> new EntityValue(player)),
+                        ((c, t) -> fromValue),
+                        ((c, t) -> fromDimStr),
+                        ((c, t) -> toValue),
+                        ((c, t) -> toDimStr)
+                ), player::getCommandSource);
+            }
+        },
+        PLAYER_CONNECTS("player_connects", 1, false) {
+            @Override
+            public void onPlayerEvent(ServerPlayerEntity player)
+            {
+                handler.call( () -> Collections.singletonList(((c, t) -> new EntityValue(player))), player::getCommandSource);
+            }
+        },
+        PLAYER_DISCONNECTS("player_disconnects", 2, false) {
+            @Override
+            public void onPlayerMessage(ServerPlayerEntity player, String message)
+            {
+                handler.call( () -> Arrays.asList(
+                        ((c, t) -> new EntityValue(player)),
+                        ((c, t) -> new StringValue(message))
+                ), player::getCommandSource);
+            }
+        },
         STATISTICS("statistic", 4, false)
         {
             private  Identifier getStatId(Stat stat)
@@ -543,6 +636,7 @@ public class CarpetEventServer
         public void onTick() { }
         public void onChunkGenerated(ServerWorld world, Chunk chunk) { }
         public void onPlayerEvent(ServerPlayerEntity player) { }
+        public void onPlayerMessage(ServerPlayerEntity player, String message) { }
         public void onPlayerStatistic(ServerPlayerEntity player, Stat stat, int amount) { }
         public void onMountControls(ServerPlayerEntity player, float strafeSpeed, float forwardSpeed, boolean jumping, boolean sneaking) { }
         public void onItemAction(ServerPlayerEntity player, Hand enumhand, ItemStack itemstack) { }
@@ -551,13 +645,18 @@ public class CarpetEventServer
         public void onBlockBroken(ServerPlayerEntity player, BlockPos pos, BlockState previousBS) { }
         public void onBlockPlaced(ServerPlayerEntity player, BlockPos pos, Hand enumhand, ItemStack itemstack) { }
         public void onEntityAction(ServerPlayerEntity player, Entity entity, Hand enumhand) { }
+        public void onDimensionChange(ServerPlayerEntity player, Vec3d from, Vec3d to, DimensionType fromDim, DimensionType dimTo) {}
+        public void onDamage(Entity target, float amount, DamageSource source) { }
+
+
         public void onWorldEvent(ServerWorld world, BlockPos pos) { }
         public void onWorldEventFlag(ServerWorld world, BlockPos pos, int flag) { }
     }
 
 
-    public CarpetEventServer()
+    public CarpetEventServer(MinecraftServer server)
     {
+        this.server = server;
         for (Event e: Event.values())
             e.handler.callList.clear();
     }
diff --git a/src/main/java/carpet/script/CarpetExpression.java b/src/main/java/carpet/script/CarpetExpression.java
index 63615671..711d07d6 100644
--- a/src/main/java/carpet/script/CarpetExpression.java
+++ b/src/main/java/carpet/script/CarpetExpression.java
@@ -1,15 +1,18 @@
 package carpet.script;
 
 import carpet.CarpetServer;
+import carpet.fakes.ChunkTicketManagerInterface;
 import carpet.fakes.MinecraftServerInterface;
 import carpet.fakes.BiomeArrayInterface;
+import carpet.fakes.ServerChunkManagerInterface;
 import carpet.fakes.StatTypeInterface;
 import carpet.fakes.ThreadedAnvilChunkStorageInterface;
 import carpet.helpers.FeatureGenerator;
-import carpet.mixins.ChunkTicketManager_scarpetMixin;
 import carpet.mixins.PointOfInterest_scarpetMixin;
-import carpet.mixins.ServerChunkManager_scarpetMixin;
 import carpet.script.Fluff.TriFunction;
+import carpet.script.argument.BlockArgument;
+import carpet.script.argument.FunctionArgument;
+import carpet.script.argument.Vector3Argument;
 import carpet.script.bundled.Module;
 import carpet.CarpetSettings;
 import carpet.script.exception.BreakStatement;
@@ -18,8 +21,10 @@ import carpet.script.exception.ContinueStatement;
 import carpet.script.exception.ExitStatement;
 import carpet.script.exception.ExpressionException;
 import carpet.script.exception.InternalExpressionException;
+import carpet.script.utils.ShapeDispatcher;
 import carpet.script.value.BlockValue;
 import carpet.script.value.EntityValue;
+import carpet.script.value.FormattedTextValue;
 import carpet.script.value.FunctionValue;
 import carpet.script.value.LazyListValue;
 import carpet.script.value.ListValue;
@@ -59,7 +64,6 @@ import net.minecraft.item.TridentItem;
 import net.minecraft.network.packet.s2c.play.ContainerSlotUpdateS2CPacket;
 import net.minecraft.network.packet.s2c.play.PlaySoundIdS2CPacket;
 import net.minecraft.command.arguments.ItemStackArgument;
-import net.minecraft.command.arguments.ParticleArgumentType;
 import net.minecraft.enchantment.Enchantments;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityType;
@@ -84,6 +88,7 @@ import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.server.world.ChunkTicket;
+import net.minecraft.server.world.ChunkTicketType;
 import net.minecraft.server.world.ServerWorld;
 import net.minecraft.sound.BlockSoundGroup;
 import net.minecraft.sound.SoundCategory;
@@ -94,6 +99,7 @@ import net.minecraft.state.property.Property;
 import net.minecraft.structure.StructurePiece;
 import net.minecraft.structure.StructureStart;
 import net.minecraft.text.LiteralText;
+import net.minecraft.util.ActionResult;
 import net.minecraft.util.Clearable;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.InvalidIdentifierException;
@@ -105,7 +111,10 @@ import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.EulerAngle;
 import net.minecraft.util.math.BlockBox;
 import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.math.Vec3i;
 import net.minecraft.util.registry.Registry;
+import net.minecraft.world.gen.feature.Feature;
+import net.minecraft.world.gen.feature.StructureFeature;
 import net.minecraft.world.poi.PointOfInterest;
 import net.minecraft.world.poi.PointOfInterestStorage;
 import net.minecraft.world.poi.PointOfInterestType;
@@ -130,6 +139,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.concurrent.CompletionException;
 import java.util.function.BiFunction;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -138,7 +148,6 @@ import static carpet.script.utils.WorldTools.canHasChunk;
 import static java.lang.Math.abs;
 import static java.lang.Math.max;
 import static java.lang.Math.min;
-import static java.lang.Math.sqrt;
 
 public class CarpetExpression
 {
@@ -174,7 +183,7 @@ public class CarpetExpression
             Value retval = test.apply(((BlockValue) v0).getBlockState(), ((BlockValue) v0).getPos()) ? Value.TRUE : Value.FALSE;
             return (c_, t_) -> retval;
         }
-        BlockValue block = BlockValue.fromParams(cc, params, 0).block;
+        BlockValue block = BlockArgument.findIn(cc, params, 0).block;
         Value retval = test.apply(block.getBlockState(), block.getPos()) ? Value.TRUE : Value.FALSE;
         return (c_, t_) -> retval;
     }
@@ -199,7 +208,7 @@ public class CarpetExpression
             Value retval = strVal != null ? new StringValue(strVal) : Value.NULL;
             return (c_, t_) -> retval;
         }
-        BlockValue block = BlockValue.fromParams(cc, params, 0).block;
+        BlockValue block = BlockArgument.findIn(cc, params, 0).block;
         String strVal = test.apply(block.getBlockState(), block.getPos());
         Value retval = strVal != null ? new StringValue(strVal) : Value.NULL;
         return (c_, t_) -> retval;
@@ -230,7 +239,7 @@ public class CarpetExpression
                 throw new InternalExpressionException("'" + name + "' function requires a block that is positioned in the world");
             }
         }
-        BlockValue block = BlockValue.fromParams(cc, params, 0).block;
+        BlockValue block = BlockArgument.findIn(cc, params, 0).block;
         Value retval = test.apply(block.getBlockState(), block.getPos(), cc.s.getWorld());
         return (c_, t_) -> retval;
     }
@@ -251,92 +260,8 @@ public class CarpetExpression
         return bs;
     }
 
-    private static final Map particleCache = new HashMap<>();
-    private ParticleEffect getParticleData(String name)
-    {
-        ParticleEffect particle = particleCache.get(name);
-        if (particle != null)
-            return particle;
-        try
-        {
-            particle = ParticleArgumentType.readParameters(new StringReader(name));
-        }
-        catch (CommandSyntaxException e)
-        {
-            throw new InternalExpressionException("No such particle: "+name);
-        }
-        particleCache.put(name, particle);
-        return particle;
-    }
 
-
-    private boolean isStraight(Vec3d from, Vec3d to, double density)
-    {
-        if ( (from.x == to.x && from.y == to.y) || (from.x == to.x && from.z == to.z) || (from.y == to.y && from.z == to.z))
-            return from.distanceTo(to) / density > 20;
-        return false;
-    }
-
-    private int drawOptimizedParticleLine(ServerWorld world, ParticleEffect particle, Vec3d from, Vec3d to, double density)
-    {
-        double distance = from.distanceTo(to);
-        int particles = (int)(distance/density);
-        Vec3d towards = to.subtract(from);
-        int parts = 0;
-        for (PlayerEntity player : world.getPlayers())
-        {
-            world.spawnParticles((ServerPlayerEntity)player, particle, true,
-                    (towards.x)/2+from.x, (towards.y)/2+from.y, (towards.z)/2+from.z, particles/3,
-                    towards.x/6, towards.y/6, towards.z/6, 0.0);
-            world.spawnParticles((ServerPlayerEntity)player, particle, true,
-                    from.x, from.y, from.z,1,0.0,0.0,0.0,0.0);
-            world.spawnParticles((ServerPlayerEntity)player, particle, true,
-                    to.x, to.y, to.z,1,0.0,0.0,0.0,0.0);
-            parts += particles/3+2;
-        }
-        int divider = 6;
-        while (particles/divider > 1)
-        {
-            int center = (divider*2)/3;
-            int dev = 2*divider;
-            for (PlayerEntity player : world.getPlayers())
-            {
-                world.spawnParticles((ServerPlayerEntity)player, particle, true,
-                        (towards.x)/center+from.x, (towards.y)/center+from.y, (towards.z)/center+from.z, particles/divider,
-                        towards.x/dev, towards.y/dev, towards.z/dev, 0.0);
-                world.spawnParticles((ServerPlayerEntity)player, particle, true,
-                        (towards.x)*(1.0-1.0/center)+from.x, (towards.y)*(1.0-1.0/center)+from.y, (towards.z)*(1.0-1.0/center)+from.z, particles/divider,
-                        towards.x/dev, towards.y/dev, towards.z/dev, 0.0);
-            }
-            parts += 2*particles/divider;
-            divider = 2*divider;
-        }
-        return parts;
-    }
-
-    private int drawParticleLine(ServerWorld world, ParticleEffect particle, Vec3d from, Vec3d to, double density)
-    {
-        if (isStraight(from, to, density)) return drawOptimizedParticleLine(world, particle, from, to, density);
-        double lineLengthSq = from.squaredDistanceTo(to);
-        if (lineLengthSq == 0) return 0;
-        Vec3d incvec = to.subtract(from).multiply(2*density/sqrt(lineLengthSq));
-        int pcount = 0;
-        for (Vec3d delta = new Vec3d(0.0,0.0,0.0);
-             delta.lengthSquared() pieces = new ArrayList<>();
+        for (StructurePiece piece : structure.getChildren())
+        {
+            BlockBox box = piece.getBoundingBox();
+            pieces.add(ListValue.of(
+                    new StringValue( NBTSerializableValue.nameFromRegistryId(Registry.STRUCTURE_PIECE.getId(piece.getType()))),
+                    (piece.getFacing()== null)?Value.NULL: new StringValue(piece.getFacing().getName()),
+                    ListValue.fromTriple(box.minX, box.minY, box.minZ),
+                    ListValue.fromTriple(box.maxX, box.maxY, box.maxZ)
+            ));
+        };
+        BlockBox boundingBox = structure.getBoundingBox();
+        Map ret = new HashMap<>();
+        ret.put(new StringValue("box"), ListValue.of(
+                ListValue.fromTriple(boundingBox.minX, boundingBox.minY, boundingBox.minZ),
+                ListValue.fromTriple(boundingBox.maxX, boundingBox.maxY, boundingBox.maxZ)
+        ));
+        ret.put(new StringValue("pieces"), ListValue.wrap(pieces));
+        return MapValue.wrap(ret);
+    }
+
     private void API_BlockManipulation()
     {
         this.expr.addLazyFunction("block", -1, (c, t, lv) ->
@@ -399,7 +350,7 @@ public class CarpetExpression
             {
                 throw new InternalExpressionException("Block requires at least one parameter");
             }
-            BlockValue retval = BlockValue.fromParams(cc, lv, 0, true).block;
+            BlockValue retval = BlockArgument.findIn(cc, lv, 0, true).block;
             // fixing block state and data
             retval.getBlockState();
             retval.getData();
@@ -413,7 +364,7 @@ public class CarpetExpression
             {
                 throw new InternalExpressionException("Block requires at least one parameter");
             }
-            CompoundTag tag = BlockValue.fromParams(cc, lv, 0, true).block.getData();
+            CompoundTag tag = BlockArgument.findIn(cc, lv, 0, true).block.getData();
             if (tag == null)
                 return (c_, t_) -> Value.NULL;
             Value retval = new NBTSerializableValue(tag);
@@ -425,7 +376,7 @@ public class CarpetExpression
         {
             CarpetContext cc = (CarpetContext) c;
             if (lv.size() == 0) throw new InternalExpressionException("'poi' requires at least one parameter");
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0, false);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0, false);
             BlockPos pos = locator.block.getPos();
             PointOfInterestStorage store = cc.s.getWorld().getPointOfInterestStorage();
             if (lv.size() == locator.offset)
@@ -475,7 +426,7 @@ public class CarpetExpression
                     return LazyValue.NULL;
             }
 
-            Value ret = ListValue.wrap(store.get(condition, pos, (int)radius, status).map( p ->
+            Value ret = ListValue.wrap(store.get(condition, pos, (int)radius, status).sorted(Comparator.comparingDouble(p -> p.getPos().getSquaredDistance(pos))).map(p ->
                     ListValue.of(
                             new StringValue(p.getType().toString()),
                             new NumericValue(p.getType().getTicketCount() - ((PointOfInterest_scarpetMixin)p).getFreeTickets()),
@@ -491,7 +442,7 @@ public class CarpetExpression
         {
             CarpetContext cc = (CarpetContext) c;
             if (lv.size() == 0) throw new InternalExpressionException("'set_poi' requires at least one parameter");
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0, false);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0, false);
             BlockPos pos = locator.block.getPos();
             if (lv.size() < locator.offset) throw new InternalExpressionException("'set_poi' requires the new poi type or null, after position argument");
             Value poi = lv.get(locator.offset+0).evalValue(c);
@@ -558,7 +509,7 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("pos_offset", -1, (c, t, lv) ->
         {
-            BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext)c, lv, 0);
+            BlockArgument locator = BlockArgument.findIn((CarpetContext)c, lv, 0);
             BlockPos pos = locator.block.getPos();
             if (lv.size() <= locator.offset)
                 throw new InternalExpressionException("'pos_offset' needs at least position, and direction");
@@ -621,7 +572,7 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("in_slime_chunk", -1, (c, t, lv) ->
         {
-            BlockPos pos = BlockValue.fromParams((CarpetContext)c, lv, 0).block.getPos();
+            BlockPos pos = BlockArgument.findIn((CarpetContext)c, lv, 0).block.getPos();
             ChunkPos chunkPos = new ChunkPos(pos);
             Value ret = new NumericValue(ChunkRandom.create(
                     chunkPos.x, chunkPos.z,
@@ -645,7 +596,7 @@ public class CarpetExpression
                 case "surface": htype = Heightmap.Type.WORLD_SURFACE; break;
                 default: throw new InternalExpressionException("Unknown heightmap type: "+type);
             }
-            BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext)c, lv, 1);
+            BlockArgument locator = BlockArgument.findIn((CarpetContext)c, lv, 1);
             BlockPos pos = locator.block.getPos();
             int x = pos.getX();
             int z = pos.getZ();
@@ -657,21 +608,21 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("loaded", -1, (c, t, lv) ->
         {
-            Value retval = ((CarpetContext) c).s.getWorld().isChunkLoaded(BlockValue.fromParams((CarpetContext) c, lv, 0).block.getPos()) ? Value.TRUE : Value.FALSE;
+            Value retval = ((CarpetContext) c).s.getWorld().isChunkLoaded(BlockArgument.findIn((CarpetContext) c, lv, 0).block.getPos()) ? Value.TRUE : Value.FALSE;
             return (c_, t_) -> retval;
         });
 
         // Deprecated, use loaded_status as more indicative
         this.expr.addLazyFunction("loaded_ep", -1, (c, t, lv) ->
         {
-            BlockPos pos = BlockValue.fromParams((CarpetContext)c, lv, 0).block.getPos();
+            BlockPos pos = BlockArgument.findIn((CarpetContext)c, lv, 0).block.getPos();
             Value retval = ((CarpetContext)c).s.getWorld().getChunkManager().shouldTickChunk(new ChunkPos(pos))?Value.TRUE : Value.FALSE;
             return (c_, t_) -> retval;
         });
 
         this.expr.addLazyFunction("loaded_status", -1, (c, t, lv) ->
         {
-            BlockPos pos = BlockValue.fromParams((CarpetContext)c, lv, 0).block.getPos();
+            BlockPos pos = BlockArgument.findIn((CarpetContext)c, lv, 0).block.getPos();
             WorldChunk chunk = ((CarpetContext)c).s.getWorld().getChunkManager().getWorldChunk(pos.getX()>>4, pos.getZ()>>4, false);
             if (chunk == null)
                 return LazyValue.ZERO;
@@ -681,7 +632,7 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("is_chunk_generated", -1, (c, t, lv) ->
         {
-            BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext)c, lv, 0);
+            BlockArgument locator = BlockArgument.findIn((CarpetContext)c, lv, 0);
             BlockPos pos = locator.block.getPos();
             boolean force = false;
             if (lv.size() > locator.offset)
@@ -692,11 +643,11 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("generation_status", -1, (c, t, lv) ->
         {
-            BlockValue.LocatorResult locatorResult = BlockValue.fromParams((CarpetContext)c, lv, 0);
-            BlockPos pos = locatorResult.block.getPos();
+            BlockArgument blockArgument = BlockArgument.findIn((CarpetContext)c, lv, 0);
+            BlockPos pos = blockArgument.block.getPos();
             boolean forceLoad = false;
-            if (lv.size() > locatorResult.offset)
-                forceLoad = lv.get(locatorResult.offset).evalValue(c, Context.BOOLEAN).getBoolean();
+            if (lv.size() > blockArgument.offset)
+                forceLoad = lv.get(blockArgument.offset).evalValue(c, Context.BOOLEAN).getBoolean();
             Chunk chunk = ((CarpetContext)c).s.getWorld().getChunk(pos.getX()>>4, pos.getZ()>>4, ChunkStatus.EMPTY, forceLoad);
             if (chunk == null)
                 return LazyValue.NULL;
@@ -708,8 +659,8 @@ public class CarpetExpression
         {
             ServerWorld world = ((CarpetContext) c).s.getWorld();
             Long2ObjectOpenHashMap>> levelTickets = (
-                    (ChunkTicketManager_scarpetMixin) ((ServerChunkManager_scarpetMixin) world.getChunkManager())
-                            .getTicketManager()
+                    (ChunkTicketManagerInterface) ((ServerChunkManagerInterface) world.getChunkManager())
+                            .getCMTicketManager()
             ).getTicketsByPosition();
             List res = new ArrayList<>();
             if (lv.size() == 0)
@@ -730,8 +681,8 @@ public class CarpetExpression
             }
             else
             {
-                BlockValue.LocatorResult locatorResult = BlockValue.fromParams((CarpetContext) c, lv, 0);
-                BlockPos pos = locatorResult.block.getPos();
+                BlockArgument blockArgument = BlockArgument.findIn((CarpetContext) c, lv, 0);
+                BlockPos pos = blockArgument.block.getPos();
                 SortedArraySet> tickets = levelTickets.get(new ChunkPos(pos).toLong());
                 if (tickets != null)
                 {
@@ -803,8 +754,8 @@ public class CarpetExpression
             ServerWorld world = cc.s.getWorld();
             if (lv.size() < 2 || lv.size() % 2 == 1)
                 throw new InternalExpressionException("'set' should have at least 2 params and odd attributes");
-            BlockValue.LocatorResult targetLocator = BlockValue.fromParams(cc, lv, 0);
-            BlockValue.LocatorResult sourceLocator = BlockValue.fromParams(cc, lv, targetLocator.offset, true);
+            BlockArgument targetLocator = BlockArgument.findIn(cc, lv, 0);
+            BlockArgument sourceLocator = BlockArgument.findIn(cc, lv, targetLocator.offset, true);
             BlockState sourceBlockState = sourceLocator.block.getBlockState();
             BlockState targetBlockState = world.getBlockState(targetLocator.block.getPos());
             if (sourceLocator.offset < lv.size())
@@ -853,7 +804,7 @@ public class CarpetExpression
         {
             CarpetContext cc = (CarpetContext)c;
             ServerWorld world = cc.s.getWorld();
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
             BlockState state = locator.block.getBlockState();
             if (state.isAir())
                 return (c_, t_) -> Value.FALSE;
@@ -963,7 +914,7 @@ public class CarpetExpression
             if (!(e instanceof ServerPlayerEntity))
                 return (c_, t_) -> Value.FALSE;
             ServerPlayerEntity player = (ServerPlayerEntity)e;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 1);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 1);
             BlockPos where = locator.block.getPos();
             BlockState state = locator.block.getBlockState();
             Block block = state.getBlock();
@@ -975,13 +926,14 @@ public class CarpetExpression
             return success ? LazyValue.TRUE : LazyValue.FALSE;
         });
 
+        // TODO rename to use_item
         this.expr.addLazyFunction("place_item", -1, (c, t, lv) ->
         {
             if (lv.size()<2)
                 throw new InternalExpressionException("'place_item' takes at least 2 parameters: item and block, or position, to place onto");
             CarpetContext cc = (CarpetContext) c;
             String itemString = lv.get(0).evalValue(c).getString();
-            BlockValue.VectorLocator locator = BlockValue.locateVec(cc, lv, 1);
+            Vector3Argument locator = Vector3Argument.findIn(cc, lv, 1);
             ItemStackArgument stackArg = NBTSerializableValue.parseItem(itemString);
             BlockPos where = new BlockPos(locator.vec);
             String facing = "up";
@@ -990,18 +942,28 @@ public class CarpetExpression
             boolean sneakPlace = false;
             if (lv.size() > locator.offset+1)
                 sneakPlace = lv.get(locator.offset+1).evalValue(c).getBoolean();
-            if (stackArg.getItem() instanceof BlockItem)
+
+            BlockValue.PlacementContext ctx;
+            try
             {
+                ctx = BlockValue.PlacementContext.from(cc.s.getWorld(), where, facing, sneakPlace, stackArg.createStack(1, false));
+            }
+            catch (CommandSyntaxException e)
+            {
+                throw new InternalExpressionException(e.getMessage());
+            }
+
+            if (!(stackArg.getItem() instanceof BlockItem))
+            {
+                ActionResult useResult = ctx.getStack().useOnBlock(ctx);
+                if (useResult == ActionResult.CONSUME || useResult == ActionResult.SUCCESS)
+                {
+                    return LazyValue.TRUE;
+                }
+            }
+            else
+            { // not sure we need special case for block items, since useOnBlock can do that as well
                 BlockItem blockItem = (BlockItem) stackArg.getItem();
-                BlockValue.PlacementContext ctx;
-                try
-                {
-                    ctx = BlockValue.PlacementContext.from(cc.s.getWorld(), where, facing, sneakPlace, stackArg.createStack(1, false));
-                }
-                catch (CommandSyntaxException e)
-                {
-                    throw new InternalExpressionException(e.getMessage());
-                }
                 if (!ctx.canPlace())
                     return (_c, _t) -> Value.FALSE;
                 BlockState placementState = blockItem.getBlock().getPlacementState(ctx);
@@ -1037,7 +999,7 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("property", -1, (c, t, lv) ->
         {
-            BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext) c, lv, 0);
+            BlockArgument locator = BlockArgument.findIn((CarpetContext) c, lv, 0);
             BlockState state = locator.block.getBlockState();
             if (lv.size() <= locator.offset)
                 throw new InternalExpressionException("'property' requires to specify a property to query");
@@ -1052,7 +1014,7 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("block_properties", -1, (c, t, lv) ->
         {
-            BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext) c, lv, 0);
+            BlockArgument locator = BlockArgument.findIn((CarpetContext) c, lv, 0);
             BlockState state = locator.block.getBlockState();
             StateManager states = state.getBlock().getStateManager();
             Value res = ListValue.wrap(states.getProperties().stream().map(
@@ -1063,7 +1025,7 @@ public class CarpetExpression
 
         this.expr.addLazyFunction("biome", -1, (c, t, lv) -> {
             CarpetContext cc = (CarpetContext)c;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
             ServerWorld world = cc.s.getWorld();
             BlockPos pos = locator.block.getPos();
             Biome biome = world.getBiome(pos);
@@ -1074,27 +1036,40 @@ public class CarpetExpression
         this.expr.addLazyFunction("set_biome", -1, (c, t, lv) ->
         {
             CarpetContext cc = (CarpetContext)c;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
             if (lv.size() == locator.offset)
                 throw new InternalExpressionException("'set_biome' needs a biome name as an argument");
             String biomeName = lv.get(locator.offset+0).evalValue(c).getString();
             Biome biome = Registry.BIOME.get(new Identifier(biomeName));
             if (biome == null)
                 throw new InternalExpressionException("Unknown biome: "+biomeName);
+            boolean doImmediateUpdate = true;
+            if (lv.size() > locator.offset+1)
+            {
+                doImmediateUpdate = lv.get(locator.offset+1).evalValue(c).getBoolean();
+            }
             ServerWorld world = cc.s.getWorld();
             BlockPos pos = locator.block.getPos();
-            Chunk chunk = world.getChunk(pos);
+            Chunk chunk = world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.BIOMES);
             ((BiomeArrayInterface)chunk.getBiomeArray()).setBiomeAtIndex(pos, world,  biome);
-            this.forceChunkUpdate(pos, world);
-            return LazyValue.NULL;
+            if (doImmediateUpdate) forceChunkUpdate(pos, world);
+            return LazyValue.TRUE;
+        });
+
+        this.expr.addLazyFunction("reload_chunk", -1, (c, t, lv) -> {
+            CarpetContext cc = (CarpetContext)c;
+            BlockPos pos = BlockArgument.findIn(cc, lv, 0).block.getPos();
+            ServerWorld world = cc.s.getWorld();
+            cc.s.getMinecraftServer().submitAndJoin( () -> forceChunkUpdate(pos, world));
+            return LazyValue.TRUE;
         });
 
         this.expr.addLazyFunction("structure_references", -1, (c, t, lv) -> {
             CarpetContext cc = (CarpetContext)c;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
             ServerWorld world = cc.s.getWorld();
             BlockPos pos = locator.block.getPos();
-            Map references = world.getChunk(pos).getStructureReferences();
+            Map references = world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.STRUCTURE_REFERENCES).getStructureReferences();
             if (lv.size() == locator.offset)
             {
                 List referenceList = references.entrySet().stream().
@@ -1114,13 +1089,59 @@ public class CarpetExpression
             return (_c, _t) -> ret;
         });
 
-        this.expr.addLazyFunction("structures", -1, (c, t, lv) -> {
+        this.expr.addLazyFunction("structure_eligibility", -1, (c, t, lv) ->
+        {
             CarpetContext cc = (CarpetContext)c;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
 
             ServerWorld world = cc.s.getWorld();
             BlockPos pos = locator.block.getPos();
-            Map structures = world.getChunk(pos).getStructureStarts();
+            StructureFeature structure = null;
+            boolean needSize = false;
+            if (lv.size() > locator.offset)
+            {
+                Value requested = lv.get(locator.offset+0).evalValue(c);
+                if (!(requested instanceof NullValue))
+                {
+                    String reqString = requested.getString();
+                    String structureName = FeatureGenerator.featureToStructure.get(reqString);
+                    if (structureName == null) throw new InternalExpressionException("Unknown structure: " + reqString);
+                    structure = Feature.STRUCTURES.get(structureName.toLowerCase(Locale.ROOT));
+                    if (structure == null) throw new InternalExpressionException("Unsupported structure: " + structureName);
+                }
+                if (lv.size() > locator.offset+1)
+                {
+                    needSize = lv.get(locator.offset+1).evalValue(c).getBoolean();
+                }
+            }
+            if (structure != null)
+            {
+                StructureStart start = FeatureGenerator.shouldStructureStartAt(world, pos, structure, needSize);
+                if (start == null) return LazyValue.NULL;
+                if (!needSize) return LazyValue.TRUE;
+                Value ret = structureToValue(start);
+                return (_c, _t) -> ret;
+            }
+            Map ret = new HashMap<>();
+            for(StructureFeature str : Feature.STRUCTURES.values())
+            {
+                StructureStart start = FeatureGenerator.shouldStructureStartAt(world, pos, str, needSize);
+                if (start == null) continue;
+
+                Value key = new StringValue(FeatureGenerator.structureToFeature.get(str.getName()).get(0));
+                ret.put(key, (!needSize)?Value.NULL:structureToValue(start));
+            }
+            Value retMap = MapValue.wrap(ret);
+            return (_c, _t) -> retMap;
+        });
+
+        this.expr.addLazyFunction("structures", -1, (c, t, lv) -> {
+            CarpetContext cc = (CarpetContext)c;
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
+
+            ServerWorld world = cc.s.getWorld();
+            BlockPos pos = locator.block.getPos();
+            Map structures = world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.STRUCTURE_STARTS).getStructureStarts();
             if (lv.size() == locator.offset)
             {
                 Map structureList = new HashMap<>();
@@ -1130,35 +1151,24 @@ public class CarpetExpression
                     if (start == StructureStart.DEFAULT)
                         continue;
                     BlockBox box = start.getBoundingBox();
-                    ListValue coord1 = ListValue.of(new NumericValue(box.minX), new NumericValue(box.minY), new NumericValue(box.minZ));
-                    ListValue coord2 = ListValue.of(new NumericValue(box.maxX), new NumericValue(box.maxY), new NumericValue(box.maxZ));
-                    structureList.put(new StringValue(FeatureGenerator.structureToFeature.get(entry.getKey()).get(0)), ListValue.of(coord1, coord2));
+                    structureList.put(
+                            new StringValue(FeatureGenerator.structureToFeature.get(entry.getKey()).get(0)),
+                            ListValue.of(ListValue.fromTriple(box.minX, box.minY, box.minZ), ListValue.fromTriple(box.maxX, box.maxY, box.maxZ))
+                    );
                 }
                 Value ret = MapValue.wrap(structureList);
                 return (_c, _t) -> ret;
             }
             String structureName = lv.get(locator.offset).evalValue(c).getString().toLowerCase(Locale.ROOT);
             StructureStart start = structures.get(FeatureGenerator.featureToStructure.get(structureName));
-            if (start == null || start == StructureStart.DEFAULT) return LazyValue.NULL;
-            List pieces = new ArrayList<>();
-            for (StructurePiece piece : start.getChildren())
-            {
-                BlockBox box = piece.getBoundingBox();
-                pieces.add(ListValue.of(
-                        new StringValue( NBTSerializableValue.nameFromRegistryId(Registry.STRUCTURE_PIECE.getId(piece.getType()))),
-                        (piece.getFacing()== null)?Value.NULL: new StringValue(piece.getFacing().getName()),
-                        ListValue.of(new NumericValue(box.minX), new NumericValue(box.minY), new NumericValue(box.minZ)),
-                        ListValue.of(new NumericValue(box.maxX), new NumericValue(box.maxY), new NumericValue(box.maxZ))
-                ));
-            }
-            Value ret = ListValue.wrap(pieces);
+            Value ret = structureToValue(start);
             return (_c, _t) -> ret;
         });
 
         this.expr.addLazyFunction("set_structure", -1, (c, t, lv) ->
         {
             CarpetContext cc = (CarpetContext)c;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
 
             ServerWorld world = cc.s.getWorld();
             BlockPos pos = locator.block.getPos();
@@ -1222,27 +1232,27 @@ public class CarpetExpression
                 {
                     List listVal = ((ListValue) first).getItems();
                     int offset = 0;
-                    BlockValue.LocatorResult locator = BlockValue.fromParamValues(cc, listVal, 0, false, false);
+                    BlockArgument locator = BlockArgument.findInValues(cc, listVal, 0, false, false);
                     requestedChunks.add(new ChunkPos(locator.block.getPos()));
                     while (listVal.size() > locator.offset)
                     {
-                        locator = BlockValue.fromParamValues(cc, listVal, locator.offset, false, false);
+                        locator = BlockArgument.findInValues(cc, listVal, locator.offset, false, false);
                         requestedChunks.add(new ChunkPos(locator.block.getPos()));
                     }
                 }
                 else
                 {
-                    BlockValue.LocatorResult locator = BlockValue.fromParamValues(cc, Collections.singletonList(first), 0, false, false);
+                    BlockArgument locator = BlockArgument.findInValues(cc, Collections.singletonList(first), 0, false, false);
                     requestedChunks.add(new ChunkPos(locator.block.getPos()));
                 }
             }
             else
             {
-                BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+                BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
                 ChunkPos from = new ChunkPos(locator.block.getPos());
                 if (lv.size() > locator.offset)
                 {
-                    locator = BlockValue.fromParams(cc, lv, locator.offset);
+                    locator = BlockArgument.findIn(cc, lv, locator.offset);
                     ChunkPos to = new ChunkPos(locator.block.getPos());
                     int xmax = Math.max(from.x, to.x);
                     int zmax = Math.max(from.z, to.z);
@@ -1270,7 +1280,7 @@ public class CarpetExpression
                 {
                     if (world.getChunk(chpos.x, chpos.z, ChunkStatus.FULL, false) != null)
                     {
-                        this.forceChunkUpdate(chpos.getCenterBlockPos(), world);
+                        forceChunkUpdate(chpos.getCenterBlockPos(), world);
                     }
                 }
                 result[0] = MapValue.wrap(report.entrySet().stream().collect(Collectors.toMap(
@@ -1284,12 +1294,60 @@ public class CarpetExpression
         this.expr.addLazyFunction("inhabited_time", -1, (c, t, lv) ->
         {
             CarpetContext cc = (CarpetContext)c;
-            BlockValue.LocatorResult locator = BlockValue.fromParams(cc, lv, 0);
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
             BlockPos pos = locator.block.getPos();
             Value ret = new NumericValue(cc.s.getWorld().getChunk(pos).getInhabitedTime());
             return (_c, _t) -> ret;
         });
 
+        this.expr.addLazyFunction("add_chunk_ticket", -1, (c, t, lv) ->
+        {
+            CarpetContext cc = (CarpetContext)c;
+            BlockArgument locator = BlockArgument.findIn(cc, lv, 0);
+            BlockPos pos = locator.block.getPos();
+            if (lv.size() != locator.offset+2) throw new InternalExpressionException("'add_chunk_ticket' requires block position, ticket type and radius");
+            String type = lv.get(locator.offset).evalValue(c).getString();
+            ChunkTicketType ticket = ticketTypes.get(type.toLowerCase(Locale.ROOT));
+            if (ticket == null) throw new InternalExpressionException("Unknown ticket type: "+type);
+            int radius = NumericValue.asNumber(lv.get(locator.offset+1).evalValue(c)).getInt();
+            if (radius < 1 || radius > 32) throw new InternalExpressionException("Ticket radius should be between 1 and 32 chunks");
+            // due to types we will wing it:
+            ChunkPos target = new ChunkPos(pos);
+            if (ticket == ChunkTicketType.PORTAL)
+                cc.s.getWorld().getChunkManager().addTicket(ChunkTicketType.PORTAL, target, radius, pos);
+            else if (ticket == ChunkTicketType.POST_TELEPORT)
+                cc.s.getWorld().getChunkManager().addTicket(ChunkTicketType.POST_TELEPORT, target, radius, 1);
+            else
+                cc.s.getWorld().getChunkManager().addTicket(ChunkTicketType.UNKNOWN, target, radius, target);
+            Value ret = new NumericValue(ticket.getExpiryTicks());
+            return (_c, _t) -> ret;
+        });
+
+    }
+    private Map> ticketTypes = new HashMap>(){{
+        put("portal", ChunkTicketType.PORTAL);
+        put("teleport", ChunkTicketType.POST_TELEPORT);
+        put("unknown", ChunkTicketType.UNKNOWN);
+    }};
+
+    private static String getScoreboardKeyFromValue(Value keyValue)
+    {
+        if (keyValue instanceof EntityValue)
+        {
+            Entity e = ((EntityValue) keyValue).getEntity();
+            if (e instanceof PlayerEntity)
+            {
+                return e.getName().getString();
+            }
+            else
+            {
+                return e.getUuidAsString();
+            }
+        }
+        else
+        {
+            return keyValue.getString();
+        }
     }
 
     private void API_Scoreboard()
@@ -1313,24 +1371,7 @@ public class CarpetExpression
                 Value ret = ListValue.wrap(scoreboard.getAllPlayerScores(objective).stream().map(s -> new StringValue(s.getPlayerName())).collect(Collectors.toList()));
                 return (_c, _t) -> ret;
             }
-            String key;
-            Value keyValue = lv.get(1).evalValue(c);
-            if (keyValue instanceof EntityValue)
-            {
-                Entity e = ((EntityValue) keyValue).getEntity();
-                if (e instanceof PlayerEntity)
-                {
-                    key = e.getDisplayName().getString();
-                }
-                else
-                {
-                    key = e.getUuidAsString();
-                }
-            }
-            else
-            {
-                key = keyValue.getString();
-            }
+            String key = getScoreboardKeyFromValue(lv.get(1).evalValue(c));
             if (!scoreboard.playerHasObjective(key, objective) && lv.size()==2)
                 return LazyValue.NULL;
             ScoreboardPlayerScore scoreboardPlayerScore = scoreboard.getPlayerScore(key, objective);
@@ -1342,15 +1383,24 @@ public class CarpetExpression
             return (_c, _t) -> retval;
         });
 
-        this.expr.addLazyFunction("scoreboard_remove", 1, (c, t, lv)-> {
+        this.expr.addLazyFunction("scoreboard_remove", -1, (c, t, lv)-> {
             CarpetContext cc = (CarpetContext)c;
             Scoreboard scoreboard =  cc.s.getMinecraftServer().getScoreboard();
             String objectiveName = lv.get(0).evalValue(c).getString();
             ScoreboardObjective objective = scoreboard.getObjective(objectiveName);
             if (objective == null)
                 return LazyValue.FALSE;
-            scoreboard.removeObjective(objective);
-            return LazyValue.TRUE;
+            if (lv.size() == 0)
+            {
+                scoreboard.removeObjective(objective);
+                return LazyValue.TRUE;
+            }
+            String key = getScoreboardKeyFromValue(lv.get(1).evalValue(c));
+            if (!scoreboard.playerHasObjective(key, objective)) return LazyValue.NULL;
+            ScoreboardPlayerScore scoreboardPlayerScore = scoreboard.getPlayerScore(key, objective);
+            Value previous = new NumericValue(scoreboardPlayerScore.getScore());
+            scoreboard.resetPlayerScore(key, objective);
+            return (c_, t_) -> previous;
         });
 
         // objective_add('lvl','level')
@@ -1764,7 +1814,7 @@ public class CarpetExpression
                  return LazyValue.NULL;
             }
 
-            BlockValue.VectorLocator position = BlockValue.locateVec(cc, lv, 1);
+            Vector3Argument position = Vector3Argument.findIn(cc, lv, 1);
             if (position.fromBlock)
                 position.vec = position.vec.subtract(0, 0.5, 0);
             CompoundTag tag = new CompoundTag();
@@ -1904,7 +1954,7 @@ public class CarpetExpression
                 ((EntityValue) v).set(what, lv.get(2).evalValue(c));
             else
                 ((EntityValue) v).set(what, ListValue.wrap(lv.subList(2, lv.size()).stream().map((vv) -> vv.evalValue(c)).collect(Collectors.toList())));
-            return lv.get(0);
+            return (cc, tt) -> v;
         });
 
         // or update
@@ -1943,30 +1993,58 @@ public class CarpetExpression
     {
         this.expr.addLazyFunction("scan", -1, (c, t, lv) ->
         {
-            int lvsise = lv.size();
-            if (lvsise != 7 && lvsise != 10)
-                throw new InternalExpressionException("'scan' takes 2, or 3 triples of coords, and the expression");
-            int cx = (int) NumericValue.asNumber(lv.get(0).evalValue(c)).getLong();
-            int cy = (int) NumericValue.asNumber(lv.get(1).evalValue(c)).getLong();
-            int cz = (int) NumericValue.asNumber(lv.get(2).evalValue(c)).getLong();
-            int xrange = (int) NumericValue.asNumber(lv.get(3).evalValue(c)).getLong();
-            int yrange = (int) NumericValue.asNumber(lv.get(4).evalValue(c)).getLong();
-            int zrange = (int) NumericValue.asNumber(lv.get(5).evalValue(c)).getLong();
-            int xprange = xrange;
-            int yprange = yrange;
-            int zprange = zrange;
-            LazyValue expr;
-            if (lvsise == 7)
+            CarpetContext cc = (CarpetContext)c;
+            BlockArgument centerLocator = BlockArgument.findIn(cc, lv, 0);
+            Vector3Argument rangeLocator = Vector3Argument.findIn(cc, lv, centerLocator.offset);
+            BlockPos center = centerLocator.block.getPos();
+            Vec3i range;
+
+            if (rangeLocator.fromBlock)
             {
-                expr = lv.get(6);
+                range = new Vec3i(
+                        abs(rangeLocator.vec.x - center.getX()),
+                        abs(rangeLocator.vec.y - center.getY()),
+                        abs(rangeLocator.vec.z - center.getZ())
+                );
+                //if (lv.size() > rangeLocator.offset+1) throw new InternalExpressionException("'scan' takes two block positions, and the expression")
             }
             else
             {
-                xprange = (int) NumericValue.asNumber(lv.get(6).evalValue(c)).getLong();
-                yprange = (int) NumericValue.asNumber(lv.get(7).evalValue(c)).getLong();
-                zprange = (int) NumericValue.asNumber(lv.get(8).evalValue(c)).getLong();
-                expr = lv.get(9);
+                range = new Vec3i(abs(rangeLocator.vec.x), abs(rangeLocator.vec.y), abs(rangeLocator.vec.z));
             }
+            Vec3i upperRange = range;
+            if (lv.size() > rangeLocator.offset+1) // +1 cause we still need the expression
+            {
+                rangeLocator = Vector3Argument.findIn(cc, lv, rangeLocator.offset);
+                if (rangeLocator.fromBlock)
+                {
+                    upperRange = new Vec3i(
+                            abs(rangeLocator.vec.x - center.getX()),
+                            abs(rangeLocator.vec.y - center.getY()),
+                            abs(rangeLocator.vec.z - center.getZ())
+                    );
+                    //if (lv.size() > rangeLocator.offset+1) throw new InternalExpressionException("'scan' takes two block positions, and the expression")
+                }
+                else
+                {
+                    upperRange = new Vec3i(abs(rangeLocator.vec.x), abs(rangeLocator.vec.y), abs(rangeLocator.vec.z));
+                }
+            }
+            if (lv.size() != rangeLocator.offset+1)
+            {
+                throw new InternalExpressionException("'scan' takes two, or three block positions, and an expression: "+lv.size()+" "+rangeLocator.offset);
+            }
+            LazyValue expr = lv.get(rangeLocator.offset);
+
+            int cx = center.getX();
+            int cy = center.getY();
+            int cz = center.getZ();
+            int xrange = range.getX();
+            int yrange = range.getY();
+            int zrange = range.getZ();
+            int xprange = upperRange.getX();
+            int yprange = upperRange.getY();
+            int zprange = upperRange.getZ();
 
             //saving outer scope
             LazyValue _x = c.getVariable("_x");
@@ -2086,7 +2164,7 @@ public class CarpetExpression
         this.expr.addLazyFunction("neighbours", -1, (c, t, lv)->
         {
 
-            BlockPos center = BlockValue.fromParams((CarpetContext) c, lv,0).block.getPos();
+            BlockPos center = BlockArgument.findIn((CarpetContext) c, lv,0).block.getPos();
             ServerWorld world = ((CarpetContext) c).s.getWorld();
 
             List neighbours = new ArrayList<>();
@@ -2378,22 +2456,61 @@ public class CarpetExpression
         });
     }
 
+    private static Map mixerMap = Arrays.stream(SoundCategory.values()).collect(Collectors.toMap(SoundCategory::getName, k -> k));
+
+    private void API_Interapperability()
+    {
+        //todo doc and test all
+        this.expr.addLazyFunction("loaded_apps", 0, (c, t, lv) ->
+        {
+            // return a set of all loaded apps
+            Value ret = new MapValue(((CarpetScriptHost) c.host).getScriptServer().modules.keySet().stream().map(StringValue::new).collect(Collectors.toSet()));
+            return (cc, tt) -> ret;
+        });
+
+        this.expr.addLazyFunction("is_app_loaded", 1, (c, t, lv) ->
+        {
+            Value ret = new NumericValue(((CarpetScriptHost) c.host).getScriptServer().modules.containsKey(lv.get(0).evalValue(c).getString()));
+            return (cc, tt) -> ret;
+        });
+
+    }
+
+    public static String recognizeResource(Value value)
+    {
+        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())
+        {
+            throw new InternalExpressionException("Cannot use "+origfile+" as resource name - must have some letters and numbers");
+        }
+        return file;
+    }
+
     private void API_AuxiliaryAspects()
     {
         this.expr.addLazyFunction("sound", -1, (c, t, lv) -> {
             CarpetContext cc = (CarpetContext)c;
             Identifier soundName = new Identifier(lv.get(0).evalValue(c).getString());
-            BlockValue.VectorLocator locator = BlockValue.locateVec(cc, lv, 1);
+            Vector3Argument locator = Vector3Argument.findIn(cc, lv, 1);
             if (Registry.SOUND_EVENT.get(soundName) == null)
                 throw new InternalExpressionException("No such sound: "+soundName.getPath());
             float volume = 1.0F;
             float pitch = 1.0F;
+            SoundCategory mixer = SoundCategory.MASTER;
             if (lv.size() > 0+locator.offset)
             {
                 volume = (float) NumericValue.asNumber(lv.get(0+locator.offset).evalValue(c)).getDouble();
                 if (lv.size() > 1+locator.offset)
                 {
                     pitch = (float) NumericValue.asNumber(lv.get(1+locator.offset).evalValue(c)).getDouble();
+                    if (lv.size() > 2+locator.offset)
+                    {
+                        String mixerName = lv.get(2+locator.offset).evalValue(c).getString();
+                        mixer = mixerMap.get(mixerName.toLowerCase(Locale.ROOT));
+                        if (mixer == null) throw  new InternalExpressionException(mixerName +" is not a valid mixer name");
+                    }
                 }
             }
             Vec3d vec = locator.vec;
@@ -2402,7 +2519,7 @@ public class CarpetExpression
             for (ServerPlayerEntity player : cc.s.getWorld().getPlayers( (p) -> p.squaredDistanceTo(vec) < d0))
             {
                 count++;
-                player.networkHandler.sendPacket(new PlaySoundIdS2CPacket(soundName, SoundCategory.PLAYERS, vec, volume, pitch));
+                player.networkHandler.sendPacket(new PlaySoundIdS2CPacket(soundName, mixer, vec, volume, pitch));
             }
             int totalPlayed = count;
             return (_c, _t) -> new NumericValue(totalPlayed);
@@ -2413,7 +2530,7 @@ public class CarpetExpression
             CarpetContext cc = (CarpetContext)c;
             MinecraftServer ms = cc.s.getMinecraftServer();
             ServerWorld world = cc.s.getWorld();
-            BlockValue.VectorLocator locator = BlockValue.locateVec(cc, lv, 1);
+            Vector3Argument locator = Vector3Argument.findIn(cc, lv, 1);
             String particleName = lv.get(0).evalValue(c).getString();
             int count = 10;
             double speed = 0;
@@ -2435,7 +2552,7 @@ public class CarpetExpression
                     }
                 }
             }
-            ParticleEffect particle = getParticleData(particleName);
+            ParticleEffect particle = ShapeDispatcher.getParticleData(particleName);
             Vec3d vec = locator.vec;
             if (player == null)
             {
@@ -2460,78 +2577,131 @@ public class CarpetExpression
             CarpetContext cc = (CarpetContext)c;
             ServerWorld world = cc.s.getWorld();
             String particleName = lv.get(0).evalValue(c).getString();
-            ParticleEffect particle = getParticleData(particleName);
-            BlockValue.VectorLocator pos1 = BlockValue.locateVec(cc, lv, 1);
-            BlockValue.VectorLocator pos2 = BlockValue.locateVec(cc, lv, pos1.offset);
-            int offset = pos2.offset;
-            double density = (lv.size() > offset)? NumericValue.asNumber(lv.get(offset).evalValue(c)).getDouble():1.0;
-            if (density <= 0)
+            ParticleEffect particle = ShapeDispatcher.getParticleData(particleName);
+            Vector3Argument pos1 = Vector3Argument.findIn(cc, lv, 1);
+            Vector3Argument pos2 = Vector3Argument.findIn(cc, lv, pos1.offset);
+            double density = 1.0;
+            ServerPlayerEntity player = null;
+            if (lv.size() > pos2.offset+0 )
             {
-                throw new InternalExpressionException("Particle density should be positive");
+                density = NumericValue.asNumber(lv.get(pos2.offset+0).evalValue(c)).getDouble();
+                if (density <= 0)
+                {
+                    throw new InternalExpressionException("Particle density should be positive");
+                }
+                if (lv.size() > pos2.offset+1)
+                {
+                    Value playerValue = lv.get(pos2.offset+1).evalValue(c);
+                    if (playerValue instanceof EntityValue)
+                    {
+                        Entity e = ((EntityValue) playerValue).getEntity();
+                        if (!(e instanceof ServerPlayerEntity)) throw new InternalExpressionException("'particle_line' player argument has to be a player");
+                        player = (ServerPlayerEntity) e;
+                    }
+                    else
+                    {
+                        player = cc.s.getMinecraftServer().getPlayerManager().getPlayer(playerValue.getString());
+                    }
+                }
             }
-            Value retval = new NumericValue(drawParticleLine(world, particle, pos1.vec, pos2.vec, density));
+
+            Value retval = new NumericValue(ShapeDispatcher.drawParticleLine(
+                    (player == null)?world.getPlayers():Collections.singletonList(player),
+                    particle, pos1.vec, pos2.vec, density));
+
             return (c_, t_) -> retval;
         });
 
-        this.expr.addLazyFunction("particle_rect", -1, (c, t, lv) ->
+        this.expr.addLazyFunction("particle_box", -1, (c, t, lv) ->
         {
             CarpetContext cc = (CarpetContext)c;
             ServerWorld world = cc.s.getWorld();
             String particleName = lv.get(0).evalValue(c).getString();
-            ParticleEffect particle = getParticleData(particleName);
-            BlockValue.VectorLocator pos1 = BlockValue.locateVec(cc, lv, 1);
-            BlockValue.VectorLocator pos2 = BlockValue.locateVec(cc, lv, pos1.offset);
-            int offset = pos2.offset;
+            ParticleEffect particle = ShapeDispatcher.getParticleData(particleName);
+            Vector3Argument pos1 = Vector3Argument.findIn(cc, lv, 1);
+            Vector3Argument pos2 = Vector3Argument.findIn(cc, lv, pos1.offset);
+
             double density = 1.0;
-            if (lv.size() > offset)
+            ServerPlayerEntity player = null;
+            if (lv.size() > pos2.offset+0 )
             {
-                density = NumericValue.asNumber(lv.get(offset).evalValue(c)).getDouble();
-            }
-            if (density <= 0)
-            {
-                throw new InternalExpressionException("Particle density should be positive");
+                density = NumericValue.asNumber(lv.get(pos2.offset+0).evalValue(c)).getDouble();
+                if (density <= 0)
+                {
+                    throw new InternalExpressionException("Particle density should be positive");
+                }
+                if (lv.size() > pos2.offset+1)
+                {
+                    Value playerValue = lv.get(pos2.offset+1).evalValue(c);
+                    if (playerValue instanceof EntityValue)
+                    {
+                        Entity e = ((EntityValue) playerValue).getEntity();
+                        if (!(e instanceof ServerPlayerEntity)) throw new InternalExpressionException("'particle_box' player argument has to be a player");
+                        player = (ServerPlayerEntity) e;
+                    }
+                    else
+                    {
+                        player = cc.s.getMinecraftServer().getPlayerManager().getPlayer(playerValue.getString());
+                    }
+                }
             }
             Vec3d a = pos1.vec;
             Vec3d b = pos2.vec;
-            double ax = min(a.x, b.x);
-            double ay = min(a.y, b.y);
-            double az = min(a.z, b.z);
-            double bx = max(a.x, b.x);
-            double by = max(a.y, b.y);
-            double bz = max(a.z, b.z);
-            int pc = 0;
-            pc += drawParticleLine(world, particle, new Vec3d(ax, ay, az), new Vec3d(ax, by, az), density);
-            pc += drawParticleLine(world, particle, new Vec3d(ax, by, az), new Vec3d(bx, by, az), density);
-            pc += drawParticleLine(world, particle, new Vec3d(bx, by, az), new Vec3d(bx, ay, az), density);
-            pc += drawParticleLine(world, particle, new Vec3d(bx, ay, az), new Vec3d(ax, ay, az), density);
-
-            pc += drawParticleLine(world, particle, new Vec3d(ax, ay, bz), new Vec3d(ax, by, bz), density);
-            pc += drawParticleLine(world, particle, new Vec3d(ax, by, bz), new Vec3d(bx, by, bz), density);
-            pc += drawParticleLine(world, particle, new Vec3d(bx, by, bz), new Vec3d(bx, ay, bz), density);
-            pc += drawParticleLine(world, particle, new Vec3d(bx, ay, bz), new Vec3d(ax, ay, bz), density);
-
-            pc += drawParticleLine(world, particle, new Vec3d(ax, ay, az), new Vec3d(ax, ay, bz), density);
-            pc += drawParticleLine(world, particle, new Vec3d(ax, by, az), new Vec3d(ax, by, bz), density);
-            pc += drawParticleLine(world, particle, new Vec3d(bx, by, az), new Vec3d(bx, by, bz), density);
-            pc += drawParticleLine(world, particle, new Vec3d(bx, ay, az), new Vec3d(bx, ay, bz), density);
-            int particleCount = pc;
+            Vec3d from = new Vec3d(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z));
+            Vec3d to = new Vec3d(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z));
+            int particleCount = ShapeDispatcher.Box.particleMesh(
+                    player==null?world.getPlayers():Collections.singletonList(player),
+                    particle, density, from, to
+            );
             return (c_, t_) -> new NumericValue(particleCount);
         });
+        // deprecated
+        this.expr.alias("particle_rect", "particle_box");
+
+
+        this.expr.addLazyFunction("draw_shape", -1, (c, t, lv) ->
+        {
+            CarpetContext cc = (CarpetContext)c;
+            ServerPlayerEntity player[] = {null};
+            List>> shapes = new ArrayList<>();
+            if (lv.size() == 1) // bulk
+            {
+                Value specLoad = lv.get(0).evalValue(c);
+                if (!(specLoad instanceof ListValue)) throw new InternalExpressionException("In bulk mode - shapes need to be provided as a list of shape specs");
+                for (Value list : ((ListValue) specLoad).getItems())
+                {
+                    if (!(list instanceof ListValue))  throw new InternalExpressionException("In bulk mode - shapes need to be provided as a list of shape specs");
+                    shapes.add( ShapeDispatcher.fromFunctionArgs(cc, ((ListValue) list).getItems(), player));
+                }
+            }
+            else
+            {
+                List params = new ArrayList<>();
+                for (LazyValue v : lv) params.add(v.evalValue(c));
+                shapes.add(ShapeDispatcher.fromFunctionArgs(cc, params, player));
+            }
+
+            ShapeDispatcher.sendShape(
+                    (player[0]==null)?cc.s.getWorld().getPlayers():Collections.singletonList(player[0]),
+                    shapes
+            );
+            return LazyValue.TRUE;
+        });
 
         this.expr.addLazyFunction("create_marker", -1, (c, t, lv) ->{
             CarpetContext cc = (CarpetContext)c;
             BlockState targetBlock = null;
-            BlockValue.VectorLocator pointLocator;
+            Vector3Argument pointLocator;
             boolean interactable = true;
             String name;
             try
             {
                 Value nameValue = lv.get(0).evalValue(c);
                 name = nameValue instanceof NullValue ? "" : nameValue.getString();
-                pointLocator = BlockValue.locateVec(cc, lv, 1, true);
+                pointLocator = Vector3Argument.findIn(cc, lv, 1, true);
                 if (lv.size()>pointLocator.offset)
                 {
-                    BlockValue.LocatorResult blockLocator = BlockValue.fromParams(cc, lv, pointLocator.offset, true, true);
+                    BlockArgument blockLocator = BlockArgument.findIn(cc, lv, pointLocator.offset, true, true);
                     if (blockLocator.block != null) targetBlock = blockLocator.block.getBlockState();
                     if (lv.size() > blockLocator.offset)
                     {
@@ -2606,17 +2776,33 @@ public class CarpetExpression
         {
             ServerCommandSource s = ((CarpetContext)c).s;
             Value res = lv.get(0).evalValue(c);
-            if (s.getEntity() instanceof  PlayerEntity)
+            if (res instanceof FormattedTextValue)
             {
-                Messenger.m((PlayerEntity) s.getEntity(), "w "+ res.getString());
+                s.sendFeedback(((FormattedTextValue) res).getText(), false);
             }
             else
             {
-                Messenger.m(s, "w "+ res.getString());
+                if (s.getEntity() instanceof PlayerEntity)
+                {
+                    Messenger.m((PlayerEntity) s.getEntity(), "w " + res.getString());
+                }
+                else
+                {
+                    Messenger.m(s, "w " + res.getString());
+                }
             }
             return (_c, _t) -> res; // pass through for variables
         });
 
+        this.expr.addLazyFunction("format", -1, (c, t, lv) -> {
+            if (lv.size() == 0 ) throw new InternalExpressionException("'format' requires at least one component");
+            List values = lv.stream().map(lazy -> lazy.evalValue(c)).collect(Collectors.toList());
+            if (values.get(0) instanceof ListValue && values.size()==1)
+                values = ((ListValue) values.get(0)).getItems();
+            Value ret = new FormattedTextValue(Messenger.c(values.stream().map(Value::getString).toArray()));
+            return (cc, tt) -> ret;
+        });
+
         //"overidden" native call to cancel if on main thread
         this.expr.addLazyFunction("task_join", 1, (c, t, lv) -> {
             if (((CarpetContext)c).s.getMinecraftServer().isOnThread())
@@ -2628,11 +2814,47 @@ public class CarpetExpression
             return (_c, _t) -> ret;
         });
 
-        this.expr.addLazyFunction("logger", 1, (c, t, lv) ->
-        {
-            Value res = lv.get(0).evalValue(c);
-            CarpetSettings.LOG.error(res.getString());
-            return (_c, _t) -> res; // pass through for variables
+        this.expr.addLazyFunctionWithDelegation("task_dock", 1, (c, t, expr, tok, lv) -> {
+            CarpetContext cc = (CarpetContext)c;
+            MinecraftServer server = cc.s.getMinecraftServer();
+            if (server.isOnThread()) return lv.get(0); // pass through for on thread tasks
+            Value[] result = new Value[]{Value.NULL};
+            RuntimeException[] internal = new RuntimeException[]{null};
+            try
+            {
+                ((CarpetContext) c).s.getMinecraftServer().submitAndJoin(() ->
+                {
+                    try
+                    {
+                        result[0] = lv.get(0).evalValue(c, t);
+                    }
+                    catch (ExpressionException exc)
+                    {
+                        internal[0] = exc;
+                    }
+                    catch (InternalExpressionException exc)
+                    {
+                        internal[0] = new ExpressionException(c, expr, tok, exc.getMessage(), exc.stack);
+                    }
+
+                    catch (ArithmeticException exc)
+                    {
+                        internal[0] = new ExpressionException(c, expr, tok, "Your math is wrong, "+exc.getMessage());
+                    }
+                });
+            }
+            catch (CompletionException exc)
+            {
+                throw new InternalExpressionException("Error while executing docked task section, internal stack trace is gone");
+            }
+            if (internal[0] != null)
+            {
+                throw internal[0];
+            }
+            Value ret = result[0]; // preventing from lazy evaluating of the result in case a future completes later
+            return (_c, _t) -> ret;
+            // pass through placeholder
+            // implmenetation should dock the task on the main thread.
         });
 
         this.expr.addLazyFunction("run", 1, (c, t, lv) -> {
@@ -2659,6 +2881,40 @@ public class CarpetExpression
             return (cc, tt) -> time;
         });
 
+        this.expr.addLazyFunction("world_time", 0, (c, t, lv) ->
+        {
+            Value time = new NumericValue(((CarpetContext) c).s.getWorld().getTime());
+            return (cc, tt) -> time;
+        });
+
+        this.expr.addLazyFunction("day_time", -1, (c, t, lv) ->
+        {
+            Value time = new NumericValue(((CarpetContext) c).s.getWorld().getTimeOfDay());
+            if (lv.size() > 0)
+            {
+                long newTime = NumericValue.asNumber(lv.get(0).evalValue(c)).getLong();
+                if (newTime < 0) newTime = 0;
+                ((CarpetContext) c).s.getWorld().setTimeOfDay(newTime);
+            }
+            return (cc, tt) -> time;
+        });
+
+        this.expr.addLazyFunction("last_tick_times", -1, (c, t, lv) ->
+        {
+            //assuming we are in the tick world section
+            // might be off one tick when run in the off tasks or asynchronously.
+            int currentReportedTick = ((CarpetContext) c).s.getMinecraftServer().getTicks()-1;
+            List ticks = new ArrayList<>(100);
+            final long[] tickArray = ((CarpetContext) c).s.getMinecraftServer().lastTickLengths;
+            for (int i=currentReportedTick+100; i > currentReportedTick; i--)
+            {
+                ticks.add(new NumericValue(((double)tickArray[i % 100])/1000000.0));
+            }
+            Value ret = ListValue.wrap(ticks);
+            return (cc, tt) -> ret;
+        });
+
+
         this.expr.addLazyFunction("game_tick", -1, (c, t, lv) -> {
             ServerCommandSource s = ((CarpetContext)c).s;
             if (!s.getMinecraftServer().isOnThread()) throw new InternalExpressionException("Unable to run ticks from threads");
@@ -2698,6 +2954,12 @@ public class CarpetExpression
             return (cc, tt) -> retval;
         });
 
+        this.expr.addLazyFunction("view_distance", 0, (c, t, lv) -> {
+            ServerCommandSource s = ((CarpetContext)c).s;
+            Value retval = new NumericValue(s.getMinecraftServer().getPlayerManager().getViewDistance());
+            return (cc, tt) -> retval;
+        });
+
         this.expr.addLazyFunction("in_dimension", 2, (c, t, lv) -> {
             ServerCommandSource outerSource = ((CarpetContext)c).s;
             Value dimensionValue = lv.get(0).evalValue(c);
@@ -2746,7 +3008,7 @@ public class CarpetExpression
         });
 
         this.expr.addLazyFunction("plop", 4, (c, t, lv) ->{
-            BlockValue.LocatorResult locator = BlockValue.fromParams((CarpetContext)c, lv, 0);
+            BlockArgument locator = BlockArgument.findIn((CarpetContext)c, lv, 0);
             if (lv.size() <= locator.offset)
                 throw new InternalExpressionException("'plop' needs extra argument indicating what to plop");
             String what = lv.get(locator.offset).evalValue(c).getString();
@@ -2758,7 +3020,7 @@ public class CarpetExpression
                 if (res == null)
                     return;
                 if (what.equalsIgnoreCase("boulder"))  // there might be more of those
-                    this.forceChunkUpdate(locator.block.getPos(), ((CarpetContext) c).s.getWorld());
+                    forceChunkUpdate(locator.block.getPos(), ((CarpetContext) c).s.getWorld());
                 result[0] = new NumericValue(res);
             });
             Value ret = result[0]; // preventing from lazy evaluating of the result in case a future completes later
@@ -2770,41 +3032,119 @@ public class CarpetExpression
                 throw new InternalExpressionException("'schedule' should have at least 2 arguments, delay and call name");
             long delay = NumericValue.asNumber(lv.get(0).evalValue(c)).getLong();
 
-            Value functionValue = lv.get(1).evalValue(c);
-            if (!(functionValue instanceof FunctionValue))
-            {
-                String name = functionValue.getString();
-                functionValue = c.host.getAssertFunction(this.expr.module, name);
-            }
-            FunctionValue function = (FunctionValue)functionValue;
+            FunctionArgument functionArgument = FunctionArgument.findIn(c, this.expr.module, lv, 1, true, true);
 
-            CarpetContext cc = (CarpetContext)c;
-            List args = new ArrayList<>();
-            for (int i=2; i < lv.size(); i++)
-            {
-                Value arg = lv.get(i).evalValue(cc);
-                args.add(arg);
-            }
-            if (function.getArguments().size() != args.size())
-                throw new InternalExpressionException("Function "+function.getString()+" takes "+
-                        function.getArguments().size()+" arguments, "+args.size()+" provided.");
-            CarpetServer.scriptServer.events.scheduleCall(cc, function, args, delay);
+            CarpetServer.scriptServer.events.scheduleCall(
+                    (CarpetContext) c,
+                    functionArgument.function,
+                    functionArgument.resolveArgs(c, Context.NONE),
+                    delay
+            );
             return (c_, t_) -> Value.TRUE;
         });
 
+        this.expr.addLazyFunction("logger", 1, (c, t, lv) ->
+        {
+            Value res = lv.get(0).evalValue(c);
+            CarpetSettings.LOG.error(res.getString());
+            return (_c, _t) -> res; // pass through for variables
+        });
+
+        this.expr.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);
+            Value retVal;
+            if (type.equals("nbt"))
+            {
+                Tag state = ((CarpetScriptHost)((CarpetContext)c).host).readFileTag(resource, shared);
+                if (state == null) return LazyValue.NULL;
+                retVal = new NBTSerializableValue(state);
+            }
+            else
+            {
+                List content = ((CarpetScriptHost) ((CarpetContext) c).host).readTextResource(resource, shared);
+                if (content == null) return LazyValue.NULL;
+                retVal = ListValue.wrap(content.stream().map(StringValue::new).collect(Collectors.toList()));
+            }
+            return (cc, tt) -> retVal;
+        });
+
+        this.expr.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;
+        });
+
+        this.expr.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);
+            boolean success;
+            if (type.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);
+            }
+            else
+            {
+                List data = new ArrayList<>();
+                if (lv.size()==3)
+                {
+                    Value val = lv.get(2).evalValue(c);
+                    if (val instanceof ListValue)
+                    {
+                        List lval = ((ListValue) val).getItems();
+                        lval.forEach(v -> data.add(v.getString()));
+                    }
+                    else
+                    {
+                        data.add(val.getString());
+                    }
+                }
+                else
+                {
+                    for(int i = 2; i < lv.size(); i++)
+                    {
+                        data.add(lv.get(i).evalValue(c).getString());
+                    }
+                }
+                success = ((CarpetScriptHost) ((CarpetContext) c).host).appendLogFile(resource, shared, type, data);
+            }
+            return success?LazyValue.TRUE:LazyValue.FALSE;
+        });
+
+        //write_file
+
         this.expr.addLazyFunction("load_app_data", -1, (c, t, lv) ->
         {
             String file = null;
+            boolean shared = false;
             if (lv.size()>0)
             {
-                String origfile = lv.get(0).evalValue(c).getString();
-                file = origfile.toLowerCase(Locale.ROOT).replaceAll("[^A-Za-z0-9]", "");
-                if (file.isEmpty())
+                file = recognizeResource(lv.get(0).evalValue(c));
+                if (lv.size() > 1)
                 {
-                    throw new InternalExpressionException("Cannot use "+file+" as resource name - must have some letters and numbers");
+                    shared = lv.get(1).evalValue(c).getBoolean();
                 }
             }
-            Tag state = ((CarpetScriptHost)((CarpetContext)c).host).getGlobalState(file);
+            Tag state = ((CarpetScriptHost)((CarpetContext)c).host).readFileTag(file, shared);
             if (state == null)
                 return (cc, tt) -> Value.NULL;
             Value retVal = new NBTSerializableValue(state);
@@ -2817,21 +3157,21 @@ public class CarpetExpression
                 throw new InternalExpressionException("'store_app_data' needs NBT tag and an optional file");
             Value val = lv.get(0).evalValue(c);
             String file = null;
+            boolean shared = false;
             if (lv.size()>1)
             {
-                String origfile = lv.get(1).evalValue(c).getString();
-                file = origfile.toLowerCase(Locale.ROOT).replaceAll("[^A-Za-z0-9]", "");
-                if (file.isEmpty())
+                file = recognizeResource(lv.get(1).evalValue(c));
+                if (lv.size() > 2)
                 {
-                    throw new InternalExpressionException("Cannot use "+file+" as resource name - must have some letters and numbers");
+                    shared = lv.get(2).evalValue(c).getBoolean();
                 }
             }
             NBTSerializableValue tagValue =  (val instanceof NBTSerializableValue)
                     ? (NBTSerializableValue) val
                     : new NBTSerializableValue(val.getString());
             Tag tag = tagValue.getTag();
-            ((CarpetScriptHost)((CarpetContext)c).host).setGlobalState(tag, file);
-            return (cc, tt) -> Value.NULL;
+            boolean success = ((CarpetScriptHost)((CarpetContext)c).host).writeTagFile(tag, file, shared);
+            return success?LazyValue.TRUE:LazyValue.FALSE;
         });
 
         this.expr.addLazyFunction("statistic", 3, (c, t, lv) ->
@@ -2890,6 +3230,7 @@ public class CarpetExpression
         API_IteratingOverAreasOfBlocks();
         API_AuxiliaryAspects();
         API_Scoreboard();
+        API_Interapperability();
     }
 
     public boolean fillAndScanCommand(ScriptHost host, int x, int y, int z)
diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java
index 0b1a5eb7..25892565 100644
--- a/src/main/java/carpet/script/CarpetScriptHost.java
+++ b/src/main/java/carpet/script/CarpetScriptHost.java
@@ -96,7 +96,7 @@ public class CarpetScriptHost extends ScriptHost
     public void addUserDefinedFunction(Context ctx, Module module, String funName, FunctionValue function)
     {
         super.addUserDefinedFunction(ctx, module, funName, function);
-        // mcarpet
+        if (ctx.host.main != module) return; // not dealing with automatic imports / exports /configs / apps from imports
         if (funName.startsWith("__")) // potential fishy activity
         {
             if (funName.startsWith("__on_")) // here we can make a determination if we want to only accept events from main module.
@@ -323,7 +323,7 @@ public class CarpetScriptHost extends ScriptHost
         {
             try
             {
-                callUDF(BlockPos.ORIGIN, CarpetServer.minecraft_server.getCommandSource(), closing, Collections.emptyList());
+                callUDF(BlockPos.ORIGIN, scriptServer.server.getCommandSource(), closing, Collections.emptyList());
             }
             catch (InvalidCallbackException ignored)
             {
@@ -334,8 +334,8 @@ public class CarpetScriptHost extends ScriptHost
             FunctionValue userClosing = value.getFunction("__on_close");
             if (userClosing != null)
             {
-                ServerPlayerEntity player = CarpetServer.minecraft_server.getPlayerManager().getPlayer(key);
-                ServerCommandSource source = (player != null)?player.getCommandSource():CarpetServer.minecraft_server.getCommandSource();
+                ServerPlayerEntity player = scriptServer.server.getPlayerManager().getPlayer(key);
+                ServerCommandSource source = (player != null)?player.getCommandSource():scriptServer.server.getCommandSource();
                 try
                 {
                     ((CarpetScriptHost) value).callUDF(BlockPos.ORIGIN, source, userClosing, Collections.emptyList());
@@ -347,7 +347,7 @@ public class CarpetScriptHost extends ScriptHost
         });
 
         String markerName = CarpetExpression.MARKER_STRING+"_"+((getName()==null)?"":getName());
-        for (ServerWorld world : CarpetServer.minecraft_server.getWorlds())
+        for (ServerWorld world : scriptServer.server.getWorlds())
         {
             for (Entity e : world.getEntities(EntityType.ARMOR_STAND, (as) -> as.getScoreboardTags().contains(markerName)))
             {
@@ -361,32 +361,31 @@ public class CarpetScriptHost extends ScriptHost
 
     private void dumpState()
     {
-        main.saveData(null, globalState);
+        Module.saveData(main, null, globalState, false);
     }
 
     private Tag loadState()
     {
-        return main.getData(null);
+        return Module.getData(main, null, false);
     }
 
-    public Tag getGlobalState(String file)
+    public Tag readFileTag(String file, boolean isShared)
     {
-        if (getName() == null ) return null;
+        if (getName() == null && !isShared) return null;
         if (file != null)
-            return main.getData(file);
+            return Module.getData(main, file, isShared);
         if (parent == null)
             return globalState;
         return ((CarpetScriptHost)parent).globalState;
     }
 
-    public void setGlobalState(Tag tag, String file)
+    public boolean writeTagFile(Tag tag, String file, boolean isShared)
     {
-        if (getName() == null ) return;
+        if (getName() == null && !isShared) return false; // if belongs to an app, cannot be default host.
 
         if (file!= null)
         {
-            main.saveData(file, tag);
-            return;
+            return Module.saveData(main, file, tag, isShared);
         }
 
         CarpetScriptHost responsibleHost = (parent != null)?(CarpetScriptHost) parent:this;
@@ -396,8 +395,28 @@ public class CarpetScriptHost extends ScriptHost
             responsibleHost.dumpState();
             responsibleHost.saveTimeout = 200;
         }
+        return true;
     }
 
+    public boolean removeResourceFile(String resource, boolean isShared, String type)
+    {
+        if (getName() == null && !isShared) return false; //
+        return Module.dropExistingFile(main, resource, type.equals("nbt")?"nbt":"txt", isShared);
+    }
+
+    public boolean appendLogFile(String resource, boolean isShared, String type, List data)
+    {
+        if (getName() == null && !isShared) return false; // if belongs to an app, cannot be default host.
+        return Module.appendToTextFile(main, resource, type, isShared, data);
+    }
+
+    public List readTextResource(String resource, boolean isShared)
+    {
+        if (getName() == null && !isShared) return null; //
+        return Module.listFile(main, resource, "txt", isShared);
+    }
+
+
     public void tick()
     {
         if (this.saveTimeout > 0)
@@ -491,4 +510,9 @@ public class CarpetScriptHost extends ScriptHost
     {
         handleErrorWithStack(message, new CarpetExpressionException(exc.getMessage(), exc.stack));
     }
+
+    public CarpetScriptServer getScriptServer()
+    {
+        return scriptServer;
+    }
 }
diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java
index aa5b0983..2f0e627d 100644
--- a/src/main/java/carpet/script/CarpetScriptServer.java
+++ b/src/main/java/carpet/script/CarpetScriptServer.java
@@ -15,6 +15,7 @@ import carpet.utils.Messenger;
 import com.mojang.brigadier.arguments.StringArgumentType;
 import com.mojang.brigadier.builder.LiteralArgumentBuilder;
 import com.mojang.brigadier.tree.CommandNode;
+import net.minecraft.server.MinecraftServer;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.util.math.BlockPos;
 
@@ -34,6 +35,7 @@ import static net.minecraft.server.command.CommandManager.literal;
 public class CarpetScriptServer
 {
     //make static for now, but will change that later:
+    public final MinecraftServer server;
     public final CarpetScriptHost globalHost;
     public final Map modules;
     public long tickStart;
@@ -47,16 +49,18 @@ public class CarpetScriptServer
         add(new BundledModule("event_test", false));
         add(new BundledModule("stats_test", false));
         add(new BundledModule("math", true));
+        add(new BundledModule("chunk_display", false));
     }};
 
-    public CarpetScriptServer()
+    public CarpetScriptServer(MinecraftServer server)
     {
+        this.server = server;
         ScriptHost.systemGlobals.clear();
-        events = new CarpetEventServer();
+        events = new CarpetEventServer(server);
         modules = new HashMap<>();
         tickStart = 0L;
         stopAll = false;
-        holyMoly = CarpetServer.minecraft_server.getCommandManager().getDispatcher().getRoot().getChildren().stream().map(CommandNode::getName).collect(Collectors.toSet());
+        holyMoly = server.getCommandManager().getDispatcher().getRoot().getChildren().stream().map(CommandNode::getName).collect(Collectors.toSet());
         globalHost = CarpetScriptHost.create(this, null, false, null);
     }
 
@@ -64,10 +68,10 @@ public class CarpetScriptServer
     {
         if (CarpetSettings.scriptsAutoload)
         {
-            Messenger.m(CarpetServer.minecraft_server.getCommandSource(), "Auto-loading world scarpet apps");
+            Messenger.m(server.getCommandSource(), "Auto-loading world scarpet apps");
             for (String moduleName: listAvailableModules(false))
             {
-                addScriptHost(CarpetServer.minecraft_server.getCommandSource(), moduleName, true, true);
+                addScriptHost(server.getCommandSource(), moduleName, true, true);
             }
         }
 
@@ -75,8 +79,8 @@ public class CarpetScriptServer
 
     public Module getModule(String name, boolean allowLibraries)
     {
-        File folder = CarpetServer.minecraft_server.getLevelStorage().resolveFile(
-                CarpetServer.minecraft_server.getLevelName(), "scripts");
+        File folder = server.getLevelStorage().resolveFile(
+                server.getLevelName(), "scripts");
         File[] listOfFiles = folder.listFiles();
         if (listOfFiles != null)
             for (File script : listOfFiles)
@@ -110,8 +114,8 @@ public class CarpetScriptServer
                 if (!mi.isLibrary()) moduleNames.add(mi.getName());
             }
         }
-        File folder = CarpetServer.minecraft_server.getLevelStorage().resolveFile(
-                CarpetServer.minecraft_server.getLevelName(), "scripts");
+        File folder = server.getLevelStorage().resolveFile(
+                server.getLevelName(), "scripts");
         File[] listOfFiles = folder.listFiles();
         if (listOfFiles == null)
             return moduleNames;
@@ -223,7 +227,7 @@ public class CarpetScriptServer
                                     })));
         }
         Messenger.m(source, "gi "+hostName+" app loaded with /"+hostName+" command");
-        CarpetServer.minecraft_server.getCommandManager().getDispatcher().register(command);
+        server.getCommandManager().getDispatcher().register(command);
         CarpetServer.settingsManager.notifyPlayersCommandsChanged();
     }
 
diff --git a/src/main/java/carpet/script/Expression.java b/src/main/java/carpet/script/Expression.java
index cfb3380b..a53f4eb5 100644
--- a/src/main/java/carpet/script/Expression.java
+++ b/src/main/java/carpet/script/Expression.java
@@ -11,6 +11,7 @@ import carpet.script.Fluff.QuadFunction;
 import carpet.script.Fluff.QuinnFunction;
 import carpet.script.Fluff.SexFunction;
 import carpet.script.Fluff.TriFunction;
+import carpet.script.argument.FunctionArgument;
 import carpet.script.bundled.Module;
 import carpet.script.exception.BreakStatement;
 import carpet.script.exception.ContinueStatement;
@@ -417,6 +418,12 @@ public class Expression
         return result;
     }
 
+    public void alias(String copy, String original)
+    {
+        functions.put(copy, functions.get(original));
+    }
+
+
     private void UserDefinedFunctionsAndControlFlow() // public just to get the javadoc right
     {
         // artificial construct to handle user defined functions and function definitions
@@ -439,19 +446,9 @@ public class Expression
             //lv.remove(lv.size()-1); // aint gonna cut it // maybe it will because of the eager eval changes
             if (t != Context.SIGNATURE) // just call the function
             {
-                Value functionValue = lv.get(0).evalValue(c);
-                if (!(functionValue instanceof FunctionValue))
-                {
-                    String name = functionValue.getString();
-                    functionValue = c.host.getAssertFunction(module, name);
-                }
-                List lvargs = new ArrayList<>(lv.size()-1);
-                for (int i=1; i< lv.size(); i++)
-                {
-                    lvargs.add(lv.get(i));
-                }
-                FunctionValue fun = (FunctionValue)functionValue;
-                Value retval = fun.callInContext(expr, c, t, fun.getExpression(), fun.getToken(), lvargs).evalValue(c);
+                FunctionArgument functionArgument = FunctionArgument.findIn(c, module, lv, 0, false, true);
+                FunctionValue fun = functionArgument.function;
+                Value retval = fun.callInContext(expr, c, t, fun.getExpression(), fun.getToken(), functionArgument.args).evalValue(c);
                 return (cc, tt) -> retval; ///!!!! dono might need to store expr and token in statics? (e? t?)
             }
             // gimme signature
@@ -657,9 +654,18 @@ public class Expression
                     throw new InternalExpressionException("Failed to resolve left hand side of the += operation");
                 }
                 Value key = ((LContainerValue) v1).getAddress();
-                Value res = cvi.get(key).add(v2);
-                cvi.put(key, res);
-                return (cc, tt) -> res;
+                Value value = cvi.get(key);
+                if (value instanceof ListValue || value instanceof MapValue)
+                {
+                    ((AbstractListValue) value).append(v2);
+                    return (cc, tt) -> value;
+                }
+                else
+                {
+                    Value res = value.add(v2);
+                    cvi.put(key, res);
+                    return (cc, tt) -> res;
+                }
             }
             v1.assertAssignable();
             String varname = v1.getVariable();
@@ -1114,7 +1120,7 @@ public class Expression
         {
             Value rval= lv.get(0).evalValue(c);
             if (!(rval instanceof AbstractListValue))
-                throw new InternalExpressionException("Second argument of 'for' function should be a list or iterator");
+                throw new InternalExpressionException("First argument of 'for' function should be a list or iterator");
             Iterator iterator = ((AbstractListValue) rval).iterator();
             LazyValue expr = lv.get(1);
             //scoping
@@ -1558,7 +1564,7 @@ public class Expression
             if (lv.size() == 1)
                 return new StringValue(format);
             int argIndex = 1;
-            if (lv.get(1) instanceof ListValue)
+            if (lv.get(1) instanceof ListValue && lv.size() == 2)
             {
                 lv = ((ListValue) lv.get(1)).getItems();
                 argIndex = 0;
@@ -1861,26 +1867,10 @@ public class Expression
         addLazyFunctionWithDelegation("task", -1, (c, t, expr, tok, lv) -> {
             if (lv.size() == 0)
                 throw new InternalExpressionException("'task' requires at least function to call as a parameter");
-            Value funcDesc = lv.get(0).evalValue(c);
-            if (!(funcDesc instanceof FunctionValue))
-            {
-                String name = funcDesc.getString();
-                funcDesc = c.host.getAssertFunction(module, name);
-            }
-            FunctionValue fun = (FunctionValue)funcDesc;
-            int extraargs = lv.size() - fun.getArguments().size();
-            if (extraargs != 1 && extraargs != 2)
-            {
-                throw new InternalExpressionException("Function takes "+fun.getArguments().size()+" arguments.");
-            }
-            List lvargs = new ArrayList<>();
-            for (int i=0; i< fun.getArguments().size(); i++)
-            {
-                lvargs.add(lv.get(i+1));
-            }
+            FunctionArgument functionArgument = FunctionArgument.findIn(c, module, lv, 0, true, false);
             Value queue = Value.NULL;
-            if (extraargs == 2) queue = lv.get(lv.size()-1).evalValue(c);
-            ThreadValue thread = new ThreadValue(queue, fun, expr, tok, c, lvargs);
+            if (lv.size() > functionArgument.offset) queue = lv.get(functionArgument.offset).evalValue(c);
+            ThreadValue thread = new ThreadValue(queue, functionArgument.function, expr, tok, c, functionArgument.args);
             Thread.yield();
             return (cc, tt) -> thread;
         });
@@ -1906,6 +1896,12 @@ public class Expression
             return ((ThreadValue) v).join();
         });
 
+        addLazyFunction("task_dock", 1, (c, t, lv) -> {
+            // pass through placeholder
+            // implmenetation should dock the task on the main thread.
+            return lv.get(0);
+        });
+
         addUnaryFunction("task_completed", (v) -> {
             if (!(v instanceof ThreadValue))
                 throw new InternalExpressionException("'task_completed' could only be used with a task value");
diff --git a/src/main/java/carpet/script/Tokenizer.java b/src/main/java/carpet/script/Tokenizer.java
index fa1890c0..ef4c0d5e 100644
--- a/src/main/java/carpet/script/Tokenizer.java
+++ b/src/main/java/carpet/script/Tokenizer.java
@@ -173,9 +173,9 @@ public class Tokenizer implements Iterator
                     }
                     else if (nextChar == 't')
                     {
-                        throw new ExpressionException(context, this.expression, token,
-                                "Tab character is not supported");
-                        //token.append('\t');
+                        //throw new ExpressionException(context, this.expression, token,
+                        //        "Tab character is not supported");
+                        token.append('\t');
                     }
                     else if (nextChar == 'r')
                     {
@@ -201,6 +201,11 @@ public class Tokenizer implements Iterator
                 {
                     token.append(input.charAt(pos++));
                     linepos++;
+                    if (ch=='\n')
+                    {
+                        lineno++;
+                        linepos = 0;
+                    }
                     if (pos == input.length() && expression != null && context != null)
                         throw new ExpressionException(context, this.expression, token, "Program truncated");
                 }
diff --git a/src/main/java/carpet/script/argument/Argument.java b/src/main/java/carpet/script/argument/Argument.java
new file mode 100644
index 00000000..1a9dbd41
--- /dev/null
+++ b/src/main/java/carpet/script/argument/Argument.java
@@ -0,0 +1,10 @@
+package carpet.script.argument;
+
+public abstract class Argument
+{
+    public int offset;
+    protected Argument(int offset)
+    {
+        this.offset = offset;
+    }
+}
diff --git a/src/main/java/carpet/script/argument/BlockArgument.java b/src/main/java/carpet/script/argument/BlockArgument.java
new file mode 100644
index 00000000..59c0d96c
--- /dev/null
+++ b/src/main/java/carpet/script/argument/BlockArgument.java
@@ -0,0 +1,144 @@
+package carpet.script.argument;
+
+import carpet.script.CarpetContext;
+import carpet.script.LazyValue;
+import carpet.script.exception.InternalExpressionException;
+import carpet.script.value.BlockValue;
+import carpet.script.value.ListValue;
+import carpet.script.value.NullValue;
+import carpet.script.value.NumericValue;
+import carpet.script.value.StringValue;
+import carpet.script.value.Value;
+import net.minecraft.util.math.BlockPos;
+
+import java.util.List;
+
+public class BlockArgument extends Argument
+{
+    public final BlockValue block;
+    private BlockArgument(BlockValue b, int o)
+    {
+        super(o);
+        block = b;
+    }
+    public static BlockArgument findIn(CarpetContext c, List params, int offset)
+    {
+        return findIn(c, params,offset, false, false);
+    }
+
+    public static BlockArgument findIn(CarpetContext c, List params, int offset, boolean acceptString)
+    {
+        return findIn(c, params,offset, acceptString, false);
+    }
+
+    public static BlockArgument findIn(CarpetContext c, List params, int offset, boolean acceptString, boolean optional)
+    {
+        try
+        {
+            Value v1 = params.get(0 + offset).evalValue(c);
+            //add conditional from string name
+            if (optional && v1 instanceof NullValue)
+            {
+                return new BlockArgument(null, 1+offset);
+            }
+            if (acceptString && v1 instanceof StringValue)
+            {
+                return new BlockArgument(BlockValue.fromString(v1.getString()), 1+offset);
+            }
+            if (v1 instanceof BlockValue)
+            {
+                return new BlockArgument(((BlockValue) v1), 1+offset);
+            }
+            if (v1 instanceof ListValue)
+            {
+                List args = ((ListValue) v1).getItems();
+                int xpos = (int) NumericValue.asNumber(args.get(0)).getLong();
+                int ypos = (int) NumericValue.asNumber(args.get(1)).getLong();
+                int zpos = (int) NumericValue.asNumber(args.get(2)).getLong();
+                return new BlockArgument(
+                        new BlockValue(
+                                null,
+                                c.s.getWorld(),
+                                new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
+                        ),
+                        1+offset);
+            }
+            int xpos = (int) NumericValue.asNumber(v1).getLong();
+            int ypos = (int) NumericValue.asNumber( params.get(1 + offset).evalValue(c)).getLong();
+            int zpos = (int) NumericValue.asNumber( params.get(2 + offset).evalValue(c)).getLong();
+            return new BlockArgument(
+                    new BlockValue(
+                            null,
+                            c.s.getWorld(),
+                            new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
+                    ),
+                    3+offset
+            );
+        }
+        catch (IndexOutOfBoundsException e)
+        {
+            throw handleError(optional, acceptString);
+        }
+    }
+
+    public static BlockArgument findInValues(CarpetContext c, List params, int offset, boolean acceptString, boolean optional)
+    {
+        try
+        {
+            Value v1 = params.get(0 + offset);
+            //add conditional from string name
+            if (optional && v1 instanceof NullValue)
+            {
+                return new BlockArgument(null, 1+offset);
+            }
+            if (acceptString && v1 instanceof StringValue)
+            {
+                return new BlockArgument(BlockValue.fromString(v1.getString()), 1+offset);
+            }
+            if (v1 instanceof BlockValue)
+            {
+                return new BlockArgument(((BlockValue) v1), 1+offset);
+            }
+            if (v1 instanceof ListValue)
+            {
+                List args = ((ListValue) v1).getItems();
+                int xpos = (int) NumericValue.asNumber(args.get(0)).getLong();
+                int ypos = (int) NumericValue.asNumber(args.get(1)).getLong();
+                int zpos = (int) NumericValue.asNumber(args.get(2)).getLong();
+                return new BlockArgument(
+                        new BlockValue(
+                                null,
+                                c.s.getWorld(),
+                                new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
+                        ),
+                        1+offset);
+            }
+            int xpos = (int) NumericValue.asNumber(v1).getLong();
+            int ypos = (int) NumericValue.asNumber( params.get(1 + offset)).getLong();
+            int zpos = (int) NumericValue.asNumber( params.get(2 + offset)).getLong();
+            return new BlockArgument(
+                    new BlockValue(
+                            null,
+                            c.s.getWorld(),
+                            new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
+                    ),
+                    3+offset
+            );
+        }
+        catch (IndexOutOfBoundsException e)
+        {
+            throw handleError(optional, acceptString);
+        }
+    }
+
+    private static InternalExpressionException handleError(boolean optional, boolean acceptString)
+    {
+        String message = "Block-type argument should be defined either by three coordinates (a triple or by three arguments), or a block value";
+        if (acceptString)
+            message+=", or a string with block description";
+        if (optional)
+            message+=", or null";
+        return new InternalExpressionException(message);
+    }
+
+}
diff --git a/src/main/java/carpet/script/argument/FunctionArgument.java b/src/main/java/carpet/script/argument/FunctionArgument.java
new file mode 100644
index 00000000..7838a518
--- /dev/null
+++ b/src/main/java/carpet/script/argument/FunctionArgument.java
@@ -0,0 +1,60 @@
+package carpet.script.argument;
+
+import carpet.script.CarpetContext;
+import carpet.script.Context;
+import carpet.script.LazyValue;
+import carpet.script.bundled.Module;
+import carpet.script.exception.InternalExpressionException;
+import carpet.script.value.FunctionValue;
+import carpet.script.value.Value;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FunctionArgument extends Argument
+{
+    public FunctionValue function;
+    public List args;
+
+    private FunctionArgument(FunctionValue function, int offset, List args)
+    {
+        super(offset);
+        this.function = function;
+        this.args = args;
+    }
+    public static FunctionArgument findIn(Context c, Module module, List params, int offset, boolean prematureEvaluation, boolean strict)
+    {
+        Value functionValue = params.get(offset).evalValue(c);
+        if (!(functionValue instanceof FunctionValue))
+        {
+            String name = functionValue.getString();
+            functionValue = c.host.getAssertFunction(module, name);
+        }
+        FunctionValue fun = (FunctionValue)functionValue;
+        int argsize = fun.getArguments().size();
+        int extraargs = params.size() - argsize - offset - 1;
+        if (extraargs < 0 || (extraargs > 0 && strict))
+            throw new InternalExpressionException("Function "+fun.getPrettyString()+" requires "+fun.getArguments().size()+" arguments");
+        List lvargs = new ArrayList<>();
+        if (prematureEvaluation)
+        {
+            for (int i = 0; i < argsize; i++)
+            {
+                Value arg = params.get(offset + 1 + i).evalValue(c);
+                lvargs.add((cc, tt) -> arg);
+            }
+        }
+        else
+        {
+            for (int i = 0; i < argsize; i++) lvargs.add(params.get(offset + 1 + i));
+        }
+        return new FunctionArgument(fun, offset+1+argsize, lvargs);
+    }
+
+    public List resolveArgs(Context context, Integer type)
+    {
+        List res = new ArrayList<>(args.size());
+        for (LazyValue arg : args) res.add(arg.evalValue(context, type));
+        return res;
+    }
+}
diff --git a/src/main/java/carpet/script/argument/Vector3Argument.java b/src/main/java/carpet/script/argument/Vector3Argument.java
new file mode 100644
index 00000000..66471aa8
--- /dev/null
+++ b/src/main/java/carpet/script/argument/Vector3Argument.java
@@ -0,0 +1,140 @@
+package carpet.script.argument;
+
+import carpet.script.CarpetContext;
+import carpet.script.LazyValue;
+import carpet.script.exception.InternalExpressionException;
+import carpet.script.value.BlockValue;
+import carpet.script.value.ListValue;
+import carpet.script.value.NumericValue;
+import carpet.script.value.Value;
+import net.minecraft.util.math.Vec3d;
+
+import java.util.List;
+
+public class Vector3Argument extends Argument
+{
+    public Vec3d vec;
+    public final double yaw;
+    public final double pitch;
+    public boolean fromBlock;
+    private Vector3Argument(Vec3d v, int o)
+    {
+        super(o);
+        this.vec = v;
+        this.yaw = 0.0D;
+        this.pitch = 0.0D;
+    }
+    private Vector3Argument(Vec3d v, int o, double y, double p)
+    {
+        super(o);
+        this.vec = v;
+        this.yaw = y;
+        this.pitch = p;
+    }
+
+    private Vector3Argument fromBlock()
+    {
+        fromBlock = true;
+        return this;
+    }
+
+
+    public static Vector3Argument findIn(CarpetContext c, List params, int offset)
+    {
+        return findIn(c,params, offset, false);
+    }
+
+    public static Vector3Argument findIn(CarpetContext c, List params, int offset, boolean optionalDirection)
+    {
+        try
+        {
+            Value v1 = params.get(0 + offset).evalValue(c);
+            if (v1 instanceof BlockValue)
+            {
+                return (new Vector3Argument(new Vec3d(((BlockValue) v1).getPos()).add(0.5,0.5,0.5), 1+offset)).fromBlock();
+            }
+            if (v1 instanceof ListValue)
+            {
+                List args = ((ListValue) v1).getItems();
+                Vec3d pos = new Vec3d(
+                        NumericValue.asNumber(args.get(0)).getDouble(),
+                        NumericValue.asNumber(args.get(1)).getDouble(),
+                        NumericValue.asNumber(args.get(2)).getDouble());
+                double yaw = 0.0D;
+                double pitch = 0.0D;
+                if (args.size()>3 && optionalDirection)
+                {
+                    yaw = NumericValue.asNumber(args.get(3)).getDouble();
+                    pitch = NumericValue.asNumber(args.get(4)).getDouble();
+                }
+                return new Vector3Argument(pos,offset+1, yaw, pitch);
+            }
+            Vec3d pos = new Vec3d(
+                    NumericValue.asNumber(v1).getDouble(),
+                    NumericValue.asNumber(params.get(1 + offset).evalValue(c)).getDouble(),
+                    NumericValue.asNumber(params.get(2 + offset).evalValue(c)).getDouble());
+            double yaw = 0.0D;
+            double pitch = 0.0D;
+            int eatenLength = 3;
+            if (params.size()>3+offset && optionalDirection)
+            {
+                yaw = NumericValue.asNumber(params.get(3 + offset).evalValue(c)).getDouble();
+                pitch = NumericValue.asNumber(params.get(4 + offset).evalValue(c)).getDouble();
+                eatenLength = 5;
+            }
+
+            return new Vector3Argument(pos,offset+eatenLength, yaw, pitch);
+        }
+        catch (IndexOutOfBoundsException e)
+        {
+            throw new InternalExpressionException("Position argument should be defined either by three coordinates (a triple or by three arguments), or a positioned block value");
+        }
+    }
+
+    public static Vector3Argument findIn(List params, int offset, boolean optionalDirection)
+    {
+        try
+        {
+            Value v1 = params.get(0 + offset);
+            if (v1 instanceof BlockValue)
+            {
+                return (new Vector3Argument(new Vec3d(((BlockValue) v1).getPos()).add(0.5,0.5,0.5), 1+offset)).fromBlock();
+            }
+            if (v1 instanceof ListValue)
+            {
+                List args = ((ListValue) v1).getItems();
+                Vec3d pos = new Vec3d(
+                        NumericValue.asNumber(args.get(0)).getDouble(),
+                        NumericValue.asNumber(args.get(1)).getDouble(),
+                        NumericValue.asNumber(args.get(2)).getDouble());
+                double yaw = 0.0D;
+                double pitch = 0.0D;
+                if (args.size()>3 && optionalDirection)
+                {
+                    yaw = NumericValue.asNumber(args.get(3)).getDouble();
+                    pitch = NumericValue.asNumber(args.get(4)).getDouble();
+                }
+                return new Vector3Argument(pos,offset+1, yaw, pitch);
+            }
+            Vec3d pos = new Vec3d(
+                    NumericValue.asNumber(v1).getDouble(),
+                    NumericValue.asNumber(params.get(1 + offset)).getDouble(),
+                    NumericValue.asNumber(params.get(2 + offset)).getDouble());
+            double yaw = 0.0D;
+            double pitch = 0.0D;
+            int eatenLength = 3;
+            if (params.size()>3+offset && optionalDirection)
+            {
+                yaw = NumericValue.asNumber(params.get(3 + offset)).getDouble();
+                pitch = NumericValue.asNumber(params.get(4 + offset)).getDouble();
+                eatenLength = 5;
+            }
+
+            return new Vector3Argument(pos,offset+eatenLength, yaw, pitch);
+        }
+        catch (IndexOutOfBoundsException e)
+        {
+            throw new InternalExpressionException("Position argument should be defined either by three coordinates (a triple or by three arguments), or a positioned block value");
+        }
+    }
+}
diff --git a/src/main/java/carpet/script/bundled/FileModule.java b/src/main/java/carpet/script/bundled/FileModule.java
index c9ff21e1..4c116eec 100644
--- a/src/main/java/carpet/script/bundled/FileModule.java
+++ b/src/main/java/carpet/script/bundled/FileModule.java
@@ -1,17 +1,24 @@
 package carpet.script.bundled;
 
-import carpet.CarpetServer;
 import net.minecraft.nbt.PositionTracker;
 import net.minecraft.nbt.Tag;
 import net.minecraft.nbt.TagReaders;
 
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 
 public class FileModule extends Module
@@ -74,7 +81,7 @@ public class FileModule extends Module
     }
 
     //copied private method from net.minecraft.nbt.NbtIo.write() and client method safe_write
-    public static void write(Tag tag_1, File file)
+    public static boolean write(Tag tag_1, File file)
     {
         File file_2 = new File(file.getAbsolutePath() + "_tmp");
         if (file_2.exists()) file_2.delete();
@@ -90,9 +97,50 @@ public class FileModule extends Module
         }
         catch (IOException e)
         {
-            return;
+            return false;
         }
         if (file.exists()) file.delete();
         if (!file.exists()) file_2.renameTo(file);
+        return true;
+    }
+
+    public static boolean appendText(File filePath, boolean addNewLines, List data)
+    {
+        try
+        {
+            OutputStream out = Files.newOutputStream(filePath.toPath(), StandardOpenOption.APPEND, StandardOpenOption.CREATE);
+            try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8)))
+            {
+                for (String line: data)
+                {
+                    writer.append(line);
+                    if (addNewLines) writer.newLine();
+                }
+            }
+        }
+        catch (IOException e)
+        {
+            return false;
+        }
+        return true;
+    }
+
+
+    public static List listFileContent(File filePath)
+    {
+        try (BufferedReader reader = Files.newBufferedReader(filePath.toPath(), StandardCharsets.UTF_8)) {
+            List result = new ArrayList<>();
+            for (;;) {
+                String line = reader.readLine();
+                if (line == null)
+                    break;
+                result.add(line.replaceAll("[\n\r]+",""));
+            }
+            return result;
+        }
+        catch (IOException e)
+        {
+            return null;
+        }
     }
 }
diff --git a/src/main/java/carpet/script/bundled/Module.java b/src/main/java/carpet/script/bundled/Module.java
index 1cc804f1..deca890e 100644
--- a/src/main/java/carpet/script/bundled/Module.java
+++ b/src/main/java/carpet/script/bundled/Module.java
@@ -4,42 +4,76 @@ import carpet.CarpetServer;
 import net.minecraft.nbt.Tag;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.file.Files;
-
-import static carpet.script.bundled.FileModule.read;
-import static carpet.script.bundled.FileModule.write;
+import java.util.List;
 
 public abstract class Module
 {
     public abstract String getName();
     public abstract String getCode();
     public abstract boolean isLibrary();
+    public final static Object writeIOSync = new Object();
 
-    public Tag getData(String file)
+    private static String getDescriptor(Module module, String file, boolean isShared)
     {
-        File dataFile = CarpetServer.minecraft_server.getLevelStorage().resolveFile(
-                CarpetServer.minecraft_server.getLevelName(), "scripts/"+getName()+".data"+(file==null?"":"."+file)+".nbt");
-        if (!Files.exists(dataFile.toPath()) || !(dataFile.isFile())) return null;
-        return read(dataFile);
+        if (isShared)
+        {
+            return "shared/"+file;
+        }
+        else if (module != null && module.getName() != null) // appdata
+        {
+            return module.getName()+".data"+(file==null?"":"/"+file);
+        }
+        else
+        {
+            throw  new RuntimeException("Invalid file descriptor: "+file);
+        }
     }
 
-    public void saveData(String file, Tag globalState)
+    public static Tag getData(Module module, String file, boolean isShared)
     {
-        File dataFile =CarpetServer.minecraft_server.getLevelStorage().resolveFile(
-                CarpetServer.minecraft_server.getLevelName(), "scripts/"+getName()+".data"+(file==null?"":"."+file)+".nbt");
-        if (!Files.exists(dataFile.toPath().getParent()))
-        {
-            try
-            {
-                Files.createDirectory(dataFile.toPath().getParent());
-            }
-            catch (IOException ignored)
-            {
-            }
-        }
-        write(globalState, dataFile);
+        File 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); }
+    }
 
+    public static boolean saveData(Module module, String file, Tag globalState, boolean isShared)
+    {
+        File 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); }
+    }
+
+    public static boolean appendToTextFile(Module module, String resourceName, String type, boolean isShared, List message)
+    {
+        File 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); }
+    }
+
+    public static boolean dropExistingFile(Module module, String resourceName, String ext, boolean isShared)
+    {
+        File dataFile = resolveResource(module, resourceName, ext, isShared);
+        if (dataFile == null) return false;
+        synchronized (writeIOSync) { return dataFile.delete(); }
+    }
+
+    public static List listFile(Module module, String resourceName, String ext, boolean isShared)
+    {
+        File dataFile = resolveResource(module, resourceName, ext, isShared);
+        if (dataFile == null) return null;
+        if (!dataFile.exists()) return null;
+        synchronized (writeIOSync) { return FileModule.listFileContent(dataFile); }
+    }
+
+    private static File resolveResource(Module module, String resourceName, String ext, boolean isShared)
+    {
+        if (!isShared && (module == null || module.getName() == null)) return null;
+        return CarpetServer.minecraft_server.getLevelStorage().resolveFile(
+                CarpetServer.minecraft_server.getLevelName(), "scripts/"+getDescriptor(module, resourceName, isShared)+"."+ext);
     }
 
     @Override
diff --git a/src/main/java/carpet/script/utils/ShapeDispatcher.java b/src/main/java/carpet/script/utils/ShapeDispatcher.java
new file mode 100644
index 00000000..cebcb143
--- /dev/null
+++ b/src/main/java/carpet/script/utils/ShapeDispatcher.java
@@ -0,0 +1,1096 @@
+package carpet.script.utils;
+
+import carpet.CarpetSettings;
+import carpet.network.ServerNetworkHandler;
+import carpet.script.CarpetContext;
+import carpet.script.Expression;
+import carpet.script.exception.InternalExpressionException;
+import carpet.script.value.BlockValue;
+import carpet.script.value.EntityValue;
+import carpet.script.value.ListValue;
+import carpet.script.value.MapValue;
+import carpet.script.value.NumericValue;
+import carpet.script.value.StringValue;
+import carpet.script.value.Value;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import net.minecraft.command.arguments.ParticleArgumentType;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.DoubleTag;
+import net.minecraft.nbt.FloatTag;
+import net.minecraft.nbt.IntTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.nbt.StringTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.particle.ParticleEffect;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.registry.Registry;
+import net.minecraft.world.World;
+import net.minecraft.world.dimension.DimensionType;
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class ShapeDispatcher
+{
+    private static final Map particleCache = new HashMap<>();
+
+    public static Pair> fromFunctionArgs(
+            CarpetContext cc,
+            List lv,
+            ServerPlayerEntity[] playerRef
+    )
+    {
+        if (lv.size() < 3) throw new InternalExpressionException("'draw_shape' takes at least three parameters, shape name, duration, and its params");
+        String shapeType = lv.get(0).getString();
+        Value duration = NumericValue.asNumber(lv.get(1), "duration");
+        Map params;
+        if (lv.size() == 3)
+        {
+            Value paramValue = lv.get(2);
+            if (paramValue instanceof MapValue)
+            {
+                params = new HashMap<>();
+                ((MapValue) paramValue).getMap().entrySet().forEach(e -> params.put(e.getKey().getString(),e.getValue()));
+            }
+            else if (paramValue instanceof ListValue)
+            {
+                params = parseParams(((ListValue) paramValue).getItems());
+            }
+            else throw new InternalExpressionException("Parameters for 'draw_shape' need to be defined either in a list or a map");
+        }
+        else
+        {
+            List paramList = new ArrayList<>();
+            for (int i=2; i < lv.size(); i++) paramList.add(lv.get(i));
+            params = ShapeDispatcher.parseParams(paramList);
+        }
+        params.putIfAbsent("dim", new StringValue(cc.s.getWorld().getDimension().getType().toString()));
+        params.putIfAbsent("duration", duration);
+
+        if (params.containsKey("player"))
+        {
+            ServerPlayerEntity player = EntityValue.getPlayerByValue(cc.s.getMinecraftServer(), params.get("player"));
+            if (player == null)
+                throw new InternalExpressionException("'player' parameter needs to represent an existing player");
+            params.remove("player");
+            playerRef[0] = player;
+        }
+        return Pair.of(ShapeDispatcher.create(cc, shapeType, params), params);
+    }
+
+    public static void sendShape(List players, List>> shapes)
+    {
+        List clientPlayers = new ArrayList<>();
+        List alternativePlayers = new ArrayList<>();
+        for (ServerPlayerEntity player : players)
+        {
+            if (ServerNetworkHandler.isValidCarpetPlayer(player))
+            {
+                clientPlayers.add(player);
+            }
+            else
+            {
+                alternativePlayers.add(player);
+            }
+        }
+        if (!clientPlayers.isEmpty())
+        {
+
+            ListTag tag = new ListTag();
+            int tagcount = 0;
+            for (Pair> s : shapes)
+            {
+                tag.add(ExpiringShape.toTag(s.getRight()));  // 4000 shapes limit boxes
+                if (tagcount++>1000)
+                {
+                    tagcount = 0;
+                    Tag finalTag = tag;
+                    clientPlayers.forEach( p -> ServerNetworkHandler.sendCustomCommand(p, "scShapes", finalTag));
+                    tag = new ListTag();
+                }
+            }
+            Tag finalTag = tag;
+            if (tag.size() > 0) clientPlayers.forEach( p -> ServerNetworkHandler.sendCustomCommand(p, "scShapes", finalTag));
+        }
+        if (!alternativePlayers.isEmpty())
+        {
+            List> alternatives = new ArrayList<>();
+            shapes.forEach(s -> alternatives.add(s.getLeft().alternative()));
+            alternativePlayers.forEach(p -> alternatives.forEach( a -> a.accept(p)));
+        }
+    }
+
+    public static ParticleEffect getParticleData(String name)
+    {
+        ParticleEffect particle = particleCache.get(name);
+        if (particle != null)
+            return particle;
+        try
+        {
+            particle = ParticleArgumentType.readParameters(new StringReader(name));
+        }
+        catch (CommandSyntaxException e)
+        {
+            throw new InternalExpressionException("No such particle: "+name);
+        }
+        particleCache.put(name, particle);
+        return particle;
+    }
+
+    public static Map parseParams(List items)
+    {
+        // parses params from API function
+        if (items.size()%2 == 1) throw new InternalExpressionException("Shape parameters list needs to be of even size");
+        Map param = new HashMap<>();
+        int i = 0;
+        while(i < items.size())
+        {
+            String name = items.get(i).getString();
+            Value val = items.get(i+1);
+            param.put(name, val);
+            i += 2;
+        }
+        return param;
+    }
+
+    public static ExpiringShape create(CarpetContext cc, String shapeType, Map userParams)
+    {
+        userParams.put("shape", new StringValue(shapeType));
+        userParams.keySet().forEach(key -> {
+            Param param = Param.of.get(key);
+            if (param==null) throw new InternalExpressionException("Unknown feature for shape: "+key);
+            userParams.put(key, param.validate(userParams, cc, userParams.get(key)));
+        });
+        Function,ExpiringShape> factory = ExpiringShape.shapeProviders.get(shapeType);
+        if (factory == null) throw new InternalExpressionException("Unknown shape: "+shapeType);
+        return factory.apply(userParams);
+    }
+
+    // client
+    public static ExpiringShape fromTag(CompoundTag tag)
+    {
+        Map options = new HashMap<>();
+        for (String key : tag.getKeys())
+        {
+            Param decoder = Param.of.get(key);
+            if (decoder==null)
+            {
+                CarpetSettings.LOG.info("Unknown parameter for shape: "+key);
+                return null;
+            }
+            Value decodedValue = decoder.decode(tag.get(key));
+            options.put(key, decodedValue);
+        }
+        Value shapeValue = options.get("shape");
+        if (shapeValue == null)
+        {
+            CarpetSettings.LOG.info("Shape id missing in "+ String.join(", ", tag.getKeys()));
+            return null;
+        }
+        Function,ExpiringShape> factory = ExpiringShape.shapeProviders.get(shapeValue.getString());
+        if (factory == null)
+        {
+            CarpetSettings.LOG.info("Unknown shape: "+shapeValue.getString());
+            return null;
+        }
+        try
+        {
+            return factory.apply(options);
+        }
+        catch (InternalExpressionException exc)
+        {
+            CarpetSettings.LOG.info(exc.getMessage());
+        }
+        return null;
+    }
+
+    public abstract static class ExpiringShape
+    {
+        public static final Map,ExpiringShape>> shapeProviders = new HashMap, ExpiringShape>>(){{
+            put("line", creator(Line::new));
+            put("box", creator(Box::new));
+            put("sphere", creator(Sphere::new));
+            put("cylinder", creator(Cylinder::new));
+        }};
+        private static Function,ExpiringShape> creator(Supplier shapeFactory)
+        {
+            return o -> {
+                ExpiringShape shape = shapeFactory.get();
+                shape.fromOptions(o);
+                return shape;
+            };
+        }
+
+        float lineWidth;
+        protected float r, g, b, a;
+        protected int color;
+        protected float fr, fg, fb, fa;
+        protected int fillColor;
+        protected int duration = 0;
+        private long key;
+        protected int followEntity;
+        protected String snapTo;
+        protected boolean snapX, snapY, snapZ;
+        public DimensionType shapeDimension;
+
+
+        protected ExpiringShape() { }
+
+        public static CompoundTag toTag(Map params)
+        {
+            CompoundTag tag = new CompoundTag();
+            params.forEach((k, v) -> {
+                Tag valTag = Param.of.get(k).toTag(v);
+                if (valTag != null) tag.put(k, valTag);
+            });
+            return tag;
+        }
+
+        private void fromOptions(Map options)
+        {
+            Set optional = optionalParams();
+            Set required = requiredParams();
+            Set all = Sets.union(optional, required);
+            if (!all.containsAll(options.keySet()))
+                throw new InternalExpressionException("Received unexpected parameters for shape: "+Sets.difference(options.keySet(), all));
+            if (!options.keySet().containsAll(required))
+                throw new InternalExpressionException("Missing required parameters for shape: "+Sets.difference(required, options.keySet()));
+            options.keySet().forEach(k ->{
+                if (!this.canTake(k))
+                    throw new InternalExpressionException("Parameter "+k+" doesn't apply for shape "+options.get("shape").getString());
+            });
+            init(options);
+        }
+
+
+
+        protected void init(Map options)
+        {
+
+            duration = NumericValue.asNumber(options.get("duration")).getInt();
+
+            lineWidth = NumericValue.asNumber(options.getOrDefault("line", optional.get("line"))).getFloat();
+
+            fillColor = NumericValue.asNumber(options.getOrDefault("fill", optional.get("fill"))).getInt();
+            this.fr = (float)(fillColor >> 24 & 0xFF) / 255.0F;
+            this.fg = (float)(fillColor >> 16 & 0xFF) / 255.0F;
+            this.fb = (float)(fillColor >>  8 & 0xFF) / 255.0F;
+            this.fa = (float)(fillColor & 0xFF) / 255.0F;
+
+            color = NumericValue.asNumber(options.getOrDefault("color", optional.get("color"))).getInt();
+            this.r = (float)(color >> 24 & 0xFF) / 255.0F;
+            this.g = (float)(color >> 16 & 0xFF) / 255.0F;
+            this.b = (float)(color >>  8 & 0xFF) / 255.0F;
+            this.a = (float)(color & 0xFF) / 255.0F;
+
+            key = 0;
+            followEntity = -1;
+            shapeDimension = Registry.DIMENSION_TYPE.get(new Identifier(options.get("dim").getString()));
+            if (options.containsKey("follow"))
+            {
+                followEntity = NumericValue.asNumber(options.getOrDefault("follow", optional.get("follow"))).getInt();
+                snapTo = options.getOrDefault("snap", optional.get("snap")).getString().toLowerCase(Locale.ROOT);
+                snapX = snapTo.contains("x");
+                snapY = snapTo.contains("y");
+                snapZ = snapTo.contains("z");
+            }
+        }
+        public int getExpiry() { return duration; }
+        public Vec3d toAbsolute(Entity e, Vec3d vec, float partialTick)
+        {
+            return vec.add(
+                    snapX?MathHelper.lerp(partialTick, e.prevX, e.getX()):0.0,
+                    snapY?MathHelper.lerp(partialTick, e.prevY, e.getY()):0.0,
+                    snapZ?MathHelper.lerp(partialTick, e.prevZ, e.getZ()):0.0
+            );
+        }
+        public Vec3d relativiseRender(World world, Vec3d vec, float partialTick)
+        {
+            if (followEntity < 0) return vec;
+            Entity e = world.getEntityById(followEntity);
+            if (e == null) return vec;
+            return toAbsolute(e, vec, partialTick);
+        }
+
+        public Vec3d vecFromValue(Value value)
+        {
+            if (!(value instanceof ListValue)) throw new InternalExpressionException("decoded value of "+value.getPrettyString()+" is not a triple");
+            List elements = ((ListValue) value).getItems();
+            return new Vec3d(
+                    NumericValue.asNumber( elements.get(0)).getDouble(),
+                    NumericValue.asNumber( elements.get(1)).getDouble(),
+                    NumericValue.asNumber( elements.get(2)).getDouble()
+            );
+        }
+        protected ParticleEffect replacementParticle()
+        {
+            String particleName = fa ==0 ?
+                    String.format(Locale.ROOT , "dust %.1f %.1f %.1f 1.0", r, g, b):
+                    String.format(Locale.ROOT , "dust %.1f %.1f %.1f 1.0", fr, fg, fb);
+            return getParticleData(particleName);
+        }
+
+
+        public abstract Consumer alternative();
+        public long key()
+        {
+            if (key!=0) return key;
+            key = calcKey();
+            return key;
+        }
+        protected long calcKey()
+        { // using FNV-1a algorithm
+            long hash = -3750763034362895579L;
+            hash ^= shapeDimension.hashCode(); hash *= 1099511628211L;
+            hash ^= color;                     hash *= 1099511628211L;
+            hash ^= followEntity;              hash *= 1099511628211L;
+            hash ^= Float.hashCode(lineWidth); hash *= 1099511628211L;
+            if (fa != 0.0) { hash = 31*hash + fillColor; hash *= 1099511628211L; }
+            return hash;
+        }
+        // list of params that need to be there
+        private final Set required = ImmutableSet.of("duration", "shape", "dim");
+        private final Map optional = ImmutableMap.of(
+                "color", new NumericValue(-1),
+                "follow", new NumericValue(-1),
+                "line", new NumericValue(2.0),
+                "fill", new NumericValue(0xffffff00),
+                "snap", new StringValue("xyz")
+        );
+        protected Set requiredParams() {return required;}
+        // list of params that can be there, with defaults
+        protected Set optionalParams() {return optional.keySet();}
+
+        private boolean canTake(String param)
+        {
+            return requiredParams().contains(param) || optionalParams().contains(param);
+        }
+    }
+
+    public static class Box extends ExpiringShape
+    {
+        private final Set required = ImmutableSet.of("from", "to");
+        private final Map optional = ImmutableMap.of();
+        @Override
+        protected Set requiredParams() { return Sets.union(super.requiredParams(), required); }
+        @Override
+        protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); }
+        public Box() { }
+
+        Vec3d from;
+        Vec3d to;
+
+        @Override
+        protected void init(Map options)
+        {
+            super.init(options);
+            from = vecFromValue(options.get("from"));
+            to = vecFromValue(options.get("to"));
+            if (from.equals(to)) throw new InternalExpressionException("Box dimensions are invalid - cannot draw a zero-size boxes");
+        }
+
+        @Override
+        public Consumer alternative()
+        {
+            ParticleEffect particle = replacementParticle();
+            double density = Math.max(2.0, from.distanceTo(to) /50) / (a+0.1);
+            return p ->
+            {
+                if (p.dimension == shapeDimension)
+                {
+                    particleMesh(
+                            Collections.singletonList(p),
+                            particle,
+                            density,
+                            relativiseRender(p.getServerWorld(), from, 0),
+                            relativiseRender(p.getServerWorld(), to, 0)
+                    );
+                }
+            };
+        }
+
+        @Override
+        public long calcKey()
+        {
+            long hash = super.calcKey();
+            hash ^= 1;                     hash *= 1099511628211L;
+            hash ^= from.hashCode();       hash *= 1099511628211L;
+            hash ^= to.hashCode();         hash *= 1099511628211L;
+            return hash;
+        }
+
+        public static int particleMesh(List playerList, ParticleEffect particle, double density,
+                                       Vec3d from, Vec3d to)
+        {
+            double x1 = from.x;
+            double y1 = from.y;
+            double z1 = from.z;
+            double x2 = to.x;
+            double y2 = to.y;
+            double z2 = to.z;
+            return
+            drawParticleLine(playerList, particle, new Vec3d(x1, y1, z1), new Vec3d(x1, y2, z1), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x1, y2, z1), new Vec3d(x2, y2, z1), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x2, y2, z1), new Vec3d(x2, y1, z1), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x2, y1, z1), new Vec3d(x1, y1, z1), density)+
+
+            drawParticleLine(playerList, particle, new Vec3d(x1, y1, z2), new Vec3d(x1, y2, z2), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x1, y2, z2), new Vec3d(x2, y2, z2), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x2, y2, z2), new Vec3d(x2, y1, z2), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x2, y1, z2), new Vec3d(x1, y1, z2), density)+
+
+            drawParticleLine(playerList, particle, new Vec3d(x1, y1, z1), new Vec3d(x1, y1, z2), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x1, y2, z1), new Vec3d(x1, y2, z2), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x2, y2, z1), new Vec3d(x2, y2, z2), density)+
+            drawParticleLine(playerList, particle, new Vec3d(x2, y1, z1), new Vec3d(x2, y1, z2), density);
+        }
+    }
+
+    public static class Line extends ExpiringShape
+    {
+        private final Set required = ImmutableSet.of("from", "to");
+        private final Map optional = ImmutableMap.of();
+        @Override
+        protected Set requiredParams() { return Sets.union(super.requiredParams(), required); }
+        @Override
+        protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); }
+
+        private Line()
+        {
+            super();
+        }
+
+        Vec3d from;
+        Vec3d to;
+
+        @Override
+        protected void init(Map options)
+        {
+            super.init(options);
+            from = vecFromValue(options.get("from"));
+            to = vecFromValue(options.get("to"));
+            if (from.equals(to)) throw new InternalExpressionException("Line dimensions are invalid - cannot draw a zero-length lines");
+        }
+
+        @Override
+        public Consumer alternative()
+        {
+            ParticleEffect particle = replacementParticle();
+            double density = Math.max(2.0, from.distanceTo(to) /50) / (a+0.1);
+            return p ->
+            {
+                if (p.dimension == shapeDimension) drawParticleLine(
+                        Collections.singletonList(p),
+                        particle,
+                        relativiseRender(p.getServerWorld(), from, 0),
+                        relativiseRender(p.getServerWorld(), to, 0),
+                        density
+                );
+            };
+        }
+
+        @Override
+        public long calcKey()
+        {
+            long hash = super.calcKey();
+            hash ^= 2;                     hash *= 1099511628211L;
+            hash ^= from.hashCode();       hash *= 1099511628211L;
+            hash ^= to.hashCode();         hash *= 1099511628211L;
+            return hash;
+        }
+    }
+
+    public static class Sphere extends ExpiringShape
+    {
+        private final Set required = ImmutableSet.of("center", "radius");
+        private final Map optional = ImmutableMap.of("level", Value.ZERO);
+        @Override
+        protected Set requiredParams() { return Sets.union(super.requiredParams(), required); }
+        @Override
+        protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); }
+
+        private Sphere()
+        {
+            super();
+        }
+
+        Vec3d center;
+        float radius;
+        int level;
+        int subdivisions;
+
+        @Override
+        protected void init(Map options)
+        {
+            super.init(options);
+            center = vecFromValue(options.get("center"));
+            radius = NumericValue.asNumber(options.get("radius")).getFloat();
+            level = NumericValue.asNumber(options.getOrDefault("level", optional.get("level"))).getInt();
+            subdivisions = level;
+            if (subdivisions <= 0)
+            {
+                subdivisions = Math.max(10, (int)(10*Math.sqrt(radius)));
+            }
+        }
+
+        @Override
+        public Consumer alternative() { return p ->
+        {
+            ParticleEffect particle = replacementParticle();
+            int partno = Math.min(1000,20*subdivisions);
+            Random rand = p.world.getRandom();
+            ServerWorld world = p.getServerWorld();
+
+            Vec3d ccenter = relativiseRender(world, center, 0 );
+
+            double ccx = ccenter.x;
+            double ccy = ccenter.y;
+            double ccz = ccenter.z;
+
+            for (int i=0; i required = ImmutableSet.of("center", "radius");
+        private final Map optional = ImmutableMap.of(
+                "level", Value.ZERO,
+                "height", Value.ZERO,
+                "axis", new StringValue("y")
+        );
+        @Override
+        protected Set requiredParams() { return Sets.union(super.requiredParams(), required); }
+        @Override
+        protected Set optionalParams() { return Sets.union(super.optionalParams(), optional.keySet()); }
+
+        Vec3d center;
+        float height;
+        float radius;
+        int level;
+        int subdivisions;
+        Direction.Axis axis;
+
+        private Cylinder() { super(); }
+
+        @Override
+        protected void init(Map options)
+        {
+            super.init(options);
+            center = vecFromValue(options.get("center"));
+            radius = NumericValue.asNumber(options.get("radius")).getFloat();
+            level = NumericValue.asNumber(options.getOrDefault("level", optional.get("level"))).getInt();
+            subdivisions = level;
+            if (subdivisions <= 0)
+            {
+                subdivisions = Math.max(10, (int)(10*Math.sqrt(radius)));
+            }
+            height = NumericValue.asNumber(options.getOrDefault("height", optional.get("height"))).getFloat();
+            axis = Direction.Axis.fromName(options.getOrDefault("axis", optional.get("axis")).getString());
+        }
+
+
+        @Override
+        public Consumer alternative() { return p ->
+        {
+            ParticleEffect particle = replacementParticle();
+            int partno = (int)Math.min(1000,Math.sqrt(20*subdivisions*(1+height)));
+            Random rand = p.world.getRandom();
+            ServerWorld world = p.getServerWorld();
+
+            Vec3d ccenter = relativiseRender(world, center, 0 );
+
+            double ccx = ccenter.x;
+            double ccy = ccenter.y;
+            double ccz = ccenter.z;
+
+            if (axis == Direction.Axis.Y)
+            {
+                for (int i=0; i of = new HashMap(){{
+            put("shape", new ShapeParam());
+            put("dim", new DimensionParam());
+            put("duration", new NonNegativeIntParam("duration"));
+            put("color", new ColorParam("color"));
+            put("follow", new EntityParam("follow"));
+            put("snap", new StringChoiceParam("snap", "xyz", "xz", "yz", "xy", "x", "y", "z"));
+            put("line", new PositiveFloatParam("line"));
+            put("fill", new ColorParam("fill"));
+
+            put("from", new Vec3Param("from", false));
+            put("to", new Vec3Param("to", true));
+            put("center", new Vec3Param("center", false));
+            put("radius", new PositiveFloatParam("radius"));
+            put("level", new PositiveIntParam("level"));
+            put("height", new FloatParam("height"));
+            put("axis", new StringChoiceParam("axis", "x", "y", "z"));
+            put("points", new PointsParam("points"));
+        }};
+        protected String id;
+        protected Param(String id)
+        {
+            this.id = id;
+        }
+
+        public abstract Tag toTag(Value value); //validates value, returning null if not necessary to keep it and serialize
+        public abstract Value validate(Map options, CarpetContext cc, Value value); // makes sure the value is proper
+        public abstract Value decode(Tag tag);
+    }
+
+    public abstract static class StringParam extends Param
+    {
+        protected StringParam(String id) { super(id); }
+
+        @Override
+        public Tag toTag(Value value) { return StringTag.of(value.getString()); }
+        public Value decode(Tag tag) { return new StringValue(tag.asString()); }
+    }
+
+    public static class StringChoiceParam extends StringParam
+    {
+        private Set options;
+        public StringChoiceParam(String id, String ... options)
+        {
+            super(id);
+            this.options = Sets.newHashSet(options);
+        }
+
+
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            if (this.options.contains(value.getString())) return value;
+            return null;
+        }
+    }
+
+    public static class DimensionParam extends StringParam
+    {
+        protected DimensionParam() { super("dim"); }
+
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            String dimStr = value.getString();
+            Optional dimOp = Registry.DIMENSION_TYPE.getOrEmpty(new Identifier(dimStr));
+            if (!dimOp.isPresent()) throw new InternalExpressionException("Unknown dimension "+dimStr);
+            return value;
+        }
+    }
+    public static class ShapeParam extends StringParam
+    {
+        protected ShapeParam() { super("shape"); }
+
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            String shape = value.getString();
+            if (!ExpiringShape.shapeProviders.containsKey(shape))
+                throw new InternalExpressionException("Unknown shape: "+shape);
+            return value;
+        }
+    }
+    public static abstract class NumericParam extends Param
+    {
+        protected NumericParam(String id) { super(id); }
+
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            if (!(value instanceof NumericValue))
+                throw new InternalExpressionException("'" + id + "' needs to be a number");
+            return value;
+        }
+    }
+    public static class FloatParam extends NumericParam
+    {
+        protected FloatParam(String id) { super(id); }
+        @Override
+        public Value decode(Tag tag) { return new NumericValue(((FloatTag)tag).getFloat()); }
+        @Override
+        public Tag toTag(Value value) { return FloatTag.of(NumericValue.asNumber(value, id).getFloat()); }
+    }
+
+    public static abstract class PositiveParam extends NumericParam
+    {
+        protected PositiveParam(String id) { super(id); }
+        @Override public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            Value ret = super.validate(options, cc, value);
+            if (((NumericValue)ret).getDouble()<=0) throw new InternalExpressionException("'"+id+"' should be positive");
+            return ret;
+        }
+    }
+    public static class PositiveFloatParam extends PositiveParam
+    {
+        protected PositiveFloatParam(String id) { super(id); }
+        @Override
+        public Value decode(Tag tag) { return new NumericValue(((FloatTag)tag).getFloat()); }
+        @Override
+        public Tag toTag(Value value) { return FloatTag.of(NumericValue.asNumber(value, id).getFloat()); }
+
+    }
+    public static class PositiveIntParam extends PositiveParam
+    {
+        protected PositiveIntParam(String id) { super(id); }
+        @Override
+        public Value decode(Tag tag) { return new NumericValue(((IntTag)tag).getInt()); }
+        @Override
+        public Tag toTag(Value value) { return IntTag.of(NumericValue.asNumber(value, id).getInt()); }
+
+    }
+    public static class NonNegativeIntParam extends NumericParam
+    {
+        protected NonNegativeIntParam(String id) { super(id); }
+        @Override
+        public Value decode(Tag tag) { return new NumericValue(((IntTag)tag).getInt()); }
+        @Override
+        public Tag toTag(Value value) { return IntTag.of(NumericValue.asNumber(value, id).getInt()); }
+        @Override public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            Value ret = super.validate(options, cc, value);
+            if (((NumericValue)ret).getDouble()<0) throw new InternalExpressionException("'"+id+"' should be non-negative");
+            return ret;
+        }
+    }
+    public static class NonNegativeFloatParam extends NumericParam
+    {
+        protected NonNegativeFloatParam(String id) { super(id); }
+        @Override
+        public Value decode(Tag tag) { return new NumericValue(((FloatTag)tag).getFloat()); }
+        @Override
+        public Tag toTag(Value value) { return FloatTag.of(NumericValue.asNumber(value, id).getFloat()); }
+        @Override public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            Value ret = super.validate(options, cc, value);
+            if (((NumericValue)ret).getDouble()<0) throw new InternalExpressionException("'"+id+"' should be non-negative");
+            return ret;
+        }
+    }
+
+
+
+
+    public static class Vec3Param extends Param
+    {
+        private boolean roundsUpForBlocks;
+        protected Vec3Param(String id, boolean doesRoundUpForBlocks)
+        {
+            super(id);
+            roundsUpForBlocks = doesRoundUpForBlocks;
+        }
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            return validate(this, options, cc, value, roundsUpForBlocks);
+        }
+        public static Value validate(Param p, Map options, CarpetContext cc, Value value, boolean roundsUp)
+        {
+            if (value instanceof BlockValue)
+            {
+                if (options.containsKey("follow"))
+                    throw new InternalExpressionException(p.id+" parameter cannot use blocks as positions for relative positioning due to 'follow' attribute being present");
+                BlockPos pos = ((BlockValue) value).getPos();
+                int offset = roundsUp?1:0;
+                return ListValue.of(
+                        new NumericValue(pos.getX()+offset),
+                        new NumericValue(pos.getY()+offset),
+                        new NumericValue(pos.getZ()+offset)
+                );
+            }
+            if (value instanceof ListValue)
+            {
+                List values = ((ListValue) value).getItems();
+                if (values.size()!=3) throw new InternalExpressionException("'"+p.id+"' requires 3 numerical values");
+                for (Value component : values)
+                {
+                    if (!(component instanceof NumericValue))
+                        throw new InternalExpressionException("'"+p.id+"' requires 3 numerical values");
+                }
+                return value;
+            }
+            if (value instanceof EntityValue)
+            {
+                if (options.containsKey("follow"))
+                    throw new InternalExpressionException(p.id+" parameter cannot use entity as positions for relative positioning due to 'follow' attribute being present");
+                Entity e = ((EntityValue) value).getEntity();
+                return ListValue.of(
+                        new NumericValue(e.getX()),
+                        new NumericValue(e.getY()),
+                        new NumericValue(e.getZ())
+                );
+            }
+            CarpetSettings.LOG.error("Value: "+value.getString());
+            throw new InternalExpressionException("'"+p.id+"' requires a triple, block or entity to indicate position");
+        }
+
+        public Value decode(Tag tag)
+        {
+            ListTag ctag = (ListTag)tag;
+            return ListValue.of(
+                    new NumericValue(ctag.getDouble(0)),
+                    new NumericValue(ctag.getDouble(1)),
+                    new NumericValue(ctag.getDouble(2))
+            );
+        }
+        @Override
+        public Tag toTag(Value value)
+        {
+            List lv = ((ListValue)value).getItems();
+            ListTag tag = new ListTag();
+            tag.add(DoubleTag.of(NumericValue.asNumber(lv.get(0), "x").getDouble()));
+            tag.add(DoubleTag.of(NumericValue.asNumber(lv.get(1), "y").getDouble()));
+            tag.add(DoubleTag.of(NumericValue.asNumber(lv.get(2), "z").getDouble()));
+            return tag;
+        }
+    }
+
+    public static class PointsParam extends Param
+    {
+        public PointsParam(String id)
+        {
+            super(id);
+        }
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            if (!(value instanceof ListValue))
+                throw new InternalExpressionException(id+ " parameter should be a list");
+            List points = new ArrayList<>();
+            for (Value point: ((ListValue) value).getItems())
+                points.add(Vec3Param.validate(this, options, cc, point, false));
+            return ListValue.wrap(points);
+        }
+
+        public Value decode(Tag tag)
+        {
+            ListTag ltag = (ListTag)tag;
+            List points = new ArrayList<>();
+            for (int i=0, ll = ltag.size(); i lv = ((ListValue)pointsValue).getItems();
+            ListTag ltag = new ListTag();
+            for (Value value : lv)
+            {
+                List coords = ((ListValue)value).getItems();
+                ListTag tag = new ListTag();
+                tag.add(DoubleTag.of(NumericValue.asNumber(lv.get(0), "x").getDouble()));
+                tag.add(DoubleTag.of(NumericValue.asNumber(lv.get(1), "y").getDouble()));
+                tag.add(DoubleTag.of(NumericValue.asNumber(lv.get(2), "z").getDouble()));
+                ltag.add(tag);
+            }
+            return ltag;
+        }
+    }
+
+
+    public static class ColorParam extends NumericParam
+    {
+        protected ColorParam(String id)
+        {
+            super(id);
+        }
+
+        public Value decode(Tag tag) { return new NumericValue(((IntTag)tag).getInt()); }
+        @Override
+        public Tag toTag(Value value) { return IntTag.of(NumericValue.asNumber(value, id).getInt()); }
+    }
+
+    public static class EntityParam extends Param
+    {
+
+        protected EntityParam(String id) { super(id); }
+
+        @Override
+        public Tag toTag(Value value)
+        {
+            return IntTag.of(NumericValue.asNumber(value, id).getInt());
+        }
+
+        @Override
+        public Value validate(Map options, CarpetContext cc, Value value)
+        {
+            if (value instanceof EntityValue) return new NumericValue(((EntityValue) value).getEntity().getEntityId());
+            ServerPlayerEntity player = EntityValue.getPlayerByValue(cc.s.getMinecraftServer(), value);
+            if (player == null)
+                throw new InternalExpressionException(id+" parameter needs to represent an entity or player");
+            return new NumericValue(player.getEntityId());
+        }
+
+        public Value decode(Tag tag) { return new NumericValue(((IntTag)tag).getInt()); }
+    }
+
+    private static boolean isStraight(Vec3d from, Vec3d to, double density)
+    {
+        if ( (from.x == to.x && from.y == to.y) || (from.x == to.x && from.z == to.z) || (from.y == to.y && from.z == to.z))
+            return from.distanceTo(to) / density > 20;
+        return false;
+    }
+
+    private static int drawOptimizedParticleLine(List playerList, ParticleEffect particle, Vec3d from, Vec3d to, double density)
+    {
+        double distance = from.distanceTo(to);
+        int particles = (int)(distance/density);
+        Vec3d towards = to.subtract(from);
+        int parts = 0;
+        for (ServerPlayerEntity player : playerList)
+        {
+            ServerWorld world = player.getServerWorld();
+            world.spawnParticles(player, particle, true,
+                    (towards.x)/2+from.x, (towards.y)/2+from.y, (towards.z)/2+from.z, particles/3,
+                    towards.x/6, towards.y/6, towards.z/6, 0.0);
+            world.spawnParticles(player, particle, true,
+                    from.x, from.y, from.z,1,0.0,0.0,0.0,0.0);
+            world.spawnParticles(player, particle, true,
+                    to.x, to.y, to.z,1,0.0,0.0,0.0,0.0);
+            parts += particles/3+2;
+        }
+        int divider = 6;
+        while (particles/divider > 1)
+        {
+            int center = (divider*2)/3;
+            int dev = 2*divider;
+            for (ServerPlayerEntity player : playerList)
+            {
+                ServerWorld world = player.getServerWorld();
+                world.spawnParticles(player, particle, true,
+                        (towards.x)/center+from.x, (towards.y)/center+from.y, (towards.z)/center+from.z, particles/divider,
+                        towards.x/dev, towards.y/dev, towards.z/dev, 0.0);
+                world.spawnParticles(player, particle, true,
+                        (towards.x)*(1.0-1.0/center)+from.x, (towards.y)*(1.0-1.0/center)+from.y, (towards.z)*(1.0-1.0/center)+from.z, particles/divider,
+                        towards.x/dev, towards.y/dev, towards.z/dev, 0.0);
+            }
+            parts += 2*particles/divider;
+            divider = 2*divider;
+        }
+        return parts;
+    }
+
+    public static int drawParticleLine(List players, ParticleEffect particle, Vec3d from, Vec3d to, double density)
+    {
+        if (isStraight(from, to, density)) return drawOptimizedParticleLine(players, particle, from, to, density);
+        double lineLengthSq = from.squaredDistanceTo(to);
+        if (lineLengthSq == 0) return 0;
+        Vec3d incvec = to.subtract(from).multiply(2*density/MathHelper.sqrt(lineLengthSq));
+        int pcount = 0;
+        for (Vec3d delta = new Vec3d(0.0,0.0,0.0);
+             delta.lengthSquared()>> shapes;
+    private MinecraftClient client;
+
+    private Map>> renderedShapes
+            = new HashMap>>()
+    {{
+        put("line", RenderedLine::new);
+        put("box", RenderedBox::new);
+        put("sphere", RenderedSphere::new);
+        put("cylinder", RenderedCylinder::new);
+    }};
+
+    public ShapesRenderer(MinecraftClient minecraftClient)
+    {
+        this.client = minecraftClient;
+        shapes = new HashMap<>();
+        shapes.put(DimensionType.OVERWORLD, new Long2ObjectOpenHashMap<>());
+        shapes.put(DimensionType.THE_NETHER, new Long2ObjectOpenHashMap<>());
+        shapes.put(DimensionType.THE_END, new Long2ObjectOpenHashMap<>());
+    }
+
+    public void render(Camera camera, float partialTick)
+    {
+        IWorld iWorld = this.client.world;
+        DimensionType dimensionType = iWorld.getDimension().getType();
+        if (shapes.get(dimensionType).isEmpty()) return;
+        long currentTime = client.world.getTime();
+        RenderSystem.disableTexture();
+        RenderSystem.enableDepthTest();
+        RenderSystem.depthFunc(515);
+        RenderSystem.enableBlend();
+        RenderSystem.defaultBlendFunc();
+        //RenderSystem.blendFuncSeparate(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE, GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ZERO);
+        //RenderSystem.shadeModel(7425);
+        RenderSystem.shadeModel(GL11.GL_FLAT);
+        RenderSystem.enableAlphaTest();
+        RenderSystem.defaultAlphaFunc();
+        //RenderSystem.alphaFunc(GL11.GL_GREATER, 0.005F);
+        RenderSystem.disableCull();
+        RenderSystem.disableLighting();
+        RenderSystem.depthMask(false);
+        //RenderSystem.polygonOffset(-3f, -3f);
+        //RenderSystem.enablePolygonOffset();
+        //Entity entity = this.client.gameRenderer.getCamera().getFocusedEntity();
+
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder bufferBuilder = tessellator.getBuffer();
+
+        // render
+        double cameraX = camera.getPos().x;
+        double cameraY = camera.getPos().y;
+        double cameraZ = camera.getPos().z;
+
+        synchronized (shapes)
+        {
+            shapes.get(dimensionType).long2ObjectEntrySet().removeIf(
+                    entry -> entry.getValue().isExpired(currentTime)
+            );
+            shapes.get(dimensionType).values().forEach(
+                    s ->
+                    {
+                        if ( s.shouldRender(dimensionType))
+                            s.renderFaces(tessellator, bufferBuilder, cameraX, cameraY, cameraZ, partialTick);
+                    }
+            );
+            //lines
+            shapes.get(dimensionType).values().forEach(
+
+                    s -> {
+                        if ( s.shouldRender(dimensionType))
+                            s.renderLines(tessellator, bufferBuilder, cameraX, cameraY, cameraZ, partialTick);
+                    }
+            );
+        }
+        RenderSystem.enableCull();
+        RenderSystem.depthMask(true);
+        RenderSystem.lineWidth(1.0F);
+        RenderSystem.enableBlend();
+        RenderSystem.defaultBlendFunc();
+        RenderSystem.enableTexture();
+        RenderSystem.shadeModel(7424);
+    }
+
+    public void addShapes(ListTag tag)
+    {
+        for (int i=0, count = tag.size(); i < count; i++)
+        {
+            addShape(tag.getCompound(i));
+        }
+    }
+
+    public void addShape(CompoundTag tag)
+    {
+        ShapeDispatcher.ExpiringShape shape = ShapeDispatcher.fromTag(tag);
+        if (shape == null) return;
+        BiFunction> shapeFactory;
+        shapeFactory = renderedShapes.get(tag.getString("shape"));
+        if (shapeFactory == null)
+        {
+            CarpetSettings.LOG.info("Unrecognized shape: "+tag.getString("shape"));
+        }
+        else
+        {
+            RenderedShape rshape = shapeFactory.apply(client, shape);
+            DimensionType dim = shape.shapeDimension;
+            long key = rshape.key();
+            synchronized (shapes)
+            {
+                RenderedShape existing = shapes.get(dim).get(key);
+                if (existing != null)
+                {   // promoting previous shape
+                    existing.expiryTick = rshape.expiryTick;
+                }
+                else
+                {
+                    shapes.get(dim).put(key, rshape);
+                }
+            }
+        }
+    }
+    public void reset()
+    {
+        synchronized (shapes)
+        {
+            shapes.values().forEach(Long2ObjectOpenHashMap::clear);
+        }
+    }
+
+
+    public abstract static class RenderedShape
+    {
+        protected T shape;
+        protected MinecraftClient client;
+        long expiryTick;
+        double renderEpsilon = 0;
+        public abstract void renderLines(Tessellator tessellator, BufferBuilder builder, double cx, double cy, double cz, float partialTick );
+        public void renderFaces(Tessellator tessellator, BufferBuilder builder, double cx, double cy, double cz, float partialTick ) {}
+        protected RenderedShape(MinecraftClient client, T shape)
+        {
+            this.shape = shape;
+            expiryTick = client.world.getTime()+shape.getExpiry();
+            renderEpsilon = (3+((double)shape.key())/Long.MAX_VALUE)/1000;
+            this.client = client;
+        }
+
+        public boolean isExpired(long currentTick)
+        {
+            return  expiryTick < currentTick;
+        }
+        public long key()
+        {
+            return shape.key();
+        };
+        public boolean shouldRender(DimensionType dim)
+        {
+            if (shape.followEntity <=0 ) return true;
+            if (client.world == null) return false;
+            if (client.world.getEntityById(shape.followEntity) == null) return false;
+            return true;
+        }
+    }
+
+    public static class RenderedBox extends RenderedShape
+    {
+
+        private RenderedBox(MinecraftClient client, ShapeDispatcher.ExpiringShape shape)
+        {
+            super(client, (ShapeDispatcher.Box)shape);
+
+        }
+        @Override
+        public void renderLines(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            if (shape.a == 0.0) return;
+            Vec3d v1 = shape.relativiseRender(client.world, shape.from, partialTick);
+            Vec3d v2 = shape.relativiseRender(client.world, shape.to, partialTick);
+            RenderSystem.lineWidth(shape.lineWidth);
+            drawBoxWireGLLines(tessellator, bufferBuilder,
+                    (float)(v1.x-cx-renderEpsilon), (float)(v1.y-cy-renderEpsilon), (float)(v1.z-cz-renderEpsilon),
+                    (float)(v2.x-cx+renderEpsilon), (float)(v2.y-cy+renderEpsilon), (float)(v2.z-cz+renderEpsilon),
+                    v1.x!=v2.x, v1.y!=v2.y, v1.z!=v2.z,
+                    shape.r, shape.g, shape.b, shape.a, shape.r, shape.g, shape.b
+            );
+        }
+        @Override
+        public void renderFaces(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            if (shape.fa == 0.0) return;
+            Vec3d v1 = shape.relativiseRender(client.world, shape.from, partialTick);
+            Vec3d v2 = shape.relativiseRender(client.world, shape.to, partialTick);
+            RenderSystem.lineWidth(1.0F);
+            drawBoxFaces(tessellator, bufferBuilder,
+                    (float)(v1.x-cx-renderEpsilon), (float)(v1.y-cy-renderEpsilon), (float)(v1.z-cz-renderEpsilon),
+                    (float)(v2.x-cx+renderEpsilon), (float)(v2.y-cy+renderEpsilon), (float)(v2.z-cz+renderEpsilon),
+                    v1.x!=v2.x, v1.y!=v2.y, v1.z!=v2.z,
+                    shape.fr, shape.fg, shape.fb, shape.fa
+            );
+        }
+
+    }
+
+    public static class RenderedLine extends RenderedShape
+    {
+        public RenderedLine(MinecraftClient client, ShapeDispatcher.ExpiringShape shape)
+        {
+            super(client, (ShapeDispatcher.Line)shape);
+        }
+        @Override
+        public void renderLines(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            Vec3d v1 = shape.relativiseRender(client.world, shape.from, partialTick);
+            Vec3d v2 = shape.relativiseRender(client.world, shape.to, partialTick);
+            RenderSystem.lineWidth(shape.lineWidth);
+            drawLine(tessellator, bufferBuilder,
+                    (float)(v1.x-cx-renderEpsilon), (float)(v1.y-cy-renderEpsilon), (float)(v1.z-cz-renderEpsilon),
+                    (float)(v2.x-cx+renderEpsilon), (float)(v2.y-cy+renderEpsilon), (float)(v2.z-cz+renderEpsilon),
+                    shape.r, shape.g, shape.b, shape.a
+            );
+        }
+    }
+
+    public static class RenderedSphere extends RenderedShape
+    {
+        public RenderedSphere(MinecraftClient client, ShapeDispatcher.ExpiringShape shape)
+        {
+            super(client, (ShapeDispatcher.Sphere)shape);
+        }
+        @Override
+        public void renderLines(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            if (shape.a == 0.0) return;
+            Vec3d vc = shape.relativiseRender(client.world, shape.center, partialTick);
+            RenderSystem.lineWidth(shape.lineWidth);
+            drawSphereWireframe(tessellator, bufferBuilder,
+                    (float)(vc.x-cx), (float)(vc.y-cy), (float)(vc.z-cz),
+                    (float)(shape.radius+renderEpsilon), shape.subdivisions,
+                    shape.r, shape.g, shape.b, shape.a);
+        }
+        @Override
+        public void renderFaces(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            if (shape.fa == 0.0) return;
+            Vec3d vc = shape.relativiseRender(client.world, shape.center, partialTick);
+            RenderSystem.lineWidth(1.0f);
+            drawSphereFaces(tessellator, bufferBuilder,
+                    (float)(vc.x-cx), (float)(vc.y-cy), (float)(vc.z-cz),
+                    (float)(shape.radius+renderEpsilon), shape.subdivisions,
+                    shape.fr, shape.fg, shape.fb, shape.fa);
+        }
+    }
+
+    public static class RenderedCylinder extends RenderedShape
+    {
+        public RenderedCylinder(MinecraftClient client, ShapeDispatcher.ExpiringShape shape)
+        {
+            super(client, (ShapeDispatcher.Cylinder)shape);
+        }
+        @Override
+        public void renderLines(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            if (shape.a == 0.0) return;
+            Vec3d vc = shape.relativiseRender(client.world, shape.center, partialTick);
+            RenderSystem.lineWidth(shape.lineWidth);
+            double dir = MathHelper.sign(shape.height);
+            drawCylinderWireframe(tessellator, bufferBuilder,
+                    (float) (vc.x - cx - dir * renderEpsilon), (float) (vc.y - cy - dir * renderEpsilon), (float) (vc.z - cz - dir * renderEpsilon),
+                    (float) (shape.radius + renderEpsilon), (float) (shape.height + 2*dir*renderEpsilon), shape.axis,
+                    shape.subdivisions, shape.radius == 0,
+                    shape.r, shape.g, shape.b, shape.a);
+
+        }
+        @Override
+        public void renderFaces(Tessellator tessellator, BufferBuilder bufferBuilder, double cx, double cy, double cz, float partialTick)
+        {
+            if (shape.fa == 0.0) return;
+            Vec3d vc = shape.relativiseRender(client.world, shape.center, partialTick);
+            RenderSystem.lineWidth(1.0f);
+            double dir = MathHelper.sign(shape.height);
+            drawCylinderFaces(tessellator, bufferBuilder,
+                    (float) (vc.x - cx - dir * renderEpsilon), (float) (vc.y - cy - dir * renderEpsilon), (float) (vc.z - cz - dir * renderEpsilon),
+                    (float) (shape.radius + renderEpsilon), (float) (shape.height + 2*dir*renderEpsilon), shape.axis,
+                    shape.subdivisions, shape.radius == 0,
+                    shape.fr, shape.fg, shape.fb, shape.fa);
+        }
+    }
+
+    // some raw shit
+
+    public static void drawLine(Tessellator tessellator, BufferBuilder builder, float x1, float y1, float z1, float x2, float y2, float z2, float red1, float grn1, float blu1, float alpha) {
+        builder.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR); // 3
+        builder.vertex(x1, y1, z1).color(red1, grn1, blu1, alpha).next();
+        builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+        tessellator.draw();
+    }
+
+    public static void drawBoxWireGLLines(
+            Tessellator tessellator, BufferBuilder builder,
+            float x1, float y1, float z1,
+            float x2, float y2, float z2,
+            boolean xthick, boolean ythick, boolean zthick,
+            float red1, float grn1, float blu1, float alpha, float red2, float grn2, float blu2)
+    {
+        builder.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR); // 3
+        if (xthick)
+        {
+            builder.vertex(x1, y1, z1).color(red1, grn2, blu2, alpha).next();
+            builder.vertex(x2, y1, z1).color(red1, grn2, blu2, alpha).next();
+
+            builder.vertex(x2, y2, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y2, z1).color(red1, grn1, blu1, alpha).next();
+
+            builder.vertex(x1, y1, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y1, z2).color(red1, grn1, blu1, alpha).next();
+
+            builder.vertex(x1, y2, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+        }
+        if (ythick)
+        {
+            builder.vertex(x1, y1, z1).color(red2, grn1, blu2, alpha).next();
+            builder.vertex(x1, y2, z1).color(red2, grn1, blu2, alpha).next();
+
+            builder.vertex(x2, y1, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y2, z1).color(red1, grn1, blu1, alpha).next();
+
+            builder.vertex(x1, y2, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y1, z2).color(red1, grn1, blu1, alpha).next();
+
+            builder.vertex(x2, y1, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+        }
+        if (zthick)
+        {
+            builder.vertex(x1, y1, z1).color(red2, grn2, blu1, alpha).next();
+            builder.vertex(x1, y1, z2).color(red2, grn2, blu1, alpha).next();
+
+            builder.vertex(x1, y2, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y2, z2).color(red1, grn1, blu1, alpha).next();
+
+            builder.vertex(x2, y1, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y1, z1).color(red1, grn1, blu1, alpha).next();
+
+            builder.vertex(x2, y2, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+        }
+        tessellator.draw();
+    }
+
+    public static void drawBoxFaces(
+            Tessellator tessellator, BufferBuilder builder,
+            float x1, float y1, float z1,
+            float x2, float y2, float z2,
+            boolean xthick, boolean ythick, boolean zthick,
+            float red1, float grn1, float blu1, float alpha)
+    {
+        builder.begin(GL11.GL_QUADS, VertexFormats.POSITION_COLOR);
+
+        if (xthick && ythick)
+        {
+            builder.vertex(x1, y1, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y1, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y2, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y2, z1).color(red1, grn1, blu1, alpha).next();
+            if (zthick)
+            {
+                builder.vertex(x1, y1, z2).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x1, y2, z2).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y1, z2).color(red1, grn1, blu1, alpha).next();
+            }
+        }
+
+
+        if (zthick && ythick)
+        {
+            builder.vertex(x1, y1, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y2, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y2, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y1, z2).color(red1, grn1, blu1, alpha).next();
+
+            if (xthick)
+            {
+                builder.vertex(x2, y1, z1).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y1, z2).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y2, z1).color(red1, grn1, blu1, alpha).next();
+            }
+        }
+
+        // now at least drawing one
+        if (zthick && xthick)
+        {
+            builder.vertex(x1, y1, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y1, z1).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x2, y1, z2).color(red1, grn1, blu1, alpha).next();
+            builder.vertex(x1, y1, z2).color(red1, grn1, blu1, alpha).next();
+
+
+            if (ythick)
+            {
+                builder.vertex(x1, y2, z1).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y2, z1).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x2, y2, z2).color(red1, grn1, blu1, alpha).next();
+                builder.vertex(x1, y2, z2).color(red1, grn1, blu1, alpha).next();
+            }
+        }
+        tessellator.draw();
+    }
+
+    public static void drawCylinderWireframe(Tessellator tessellator, BufferBuilder builder,
+                                             float cx, float cy, float cz,
+                                             float r, float h, Direction.Axis axis,  int subd, boolean isFlat,
+                                             float red, float grn, float blu, float alpha)
+    {
+        float step = (float)Math.PI / (subd/2);
+        int num_steps180 = (int)(Math.PI / step)+1;
+        int num_steps360 = (int)(2*Math.PI / step);
+        int hsteps = 1;
+        float hstep = 1.0f;
+        if (!isFlat)
+        {
+            hsteps = (int) Math.ceil(MathHelper.abs(h) / (step * r)) + 1;
+            hstep = h / (hsteps - 1);
+        }// draw base
+
+        if (axis == Direction.Axis.Y)
+        {
+            for (int dh = 0; dh < hsteps; dh++)
+            {
+                float hh = dh*hstep;
+                builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float y = hh;
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(x + cx, y + cy, z + cz).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+
+            if (!isFlat)
+            {
+                for (int i = 0; i <= num_steps180; i++)
+                {
+                    builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+
+                    float z = r * MathHelper.sin(theta);
+
+                    builder.vertex(cx - x, cy + 0, cz + z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + x, cy + 0, cz - z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + x, cy + h, cz - z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx - x, cy + h, cz + z).color(red, grn, blu, alpha).next();
+                    tessellator.draw();
+                }
+            }
+            else
+            {
+                builder.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps180; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(cx - x, cy, cz + z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + x, cy, cz - z).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+
+        }
+        else if (axis == Direction.Axis.X)
+        {
+            for (int dh = 0; dh < hsteps; dh++)
+            {
+                float hh = dh * hstep;
+                builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float z = r * MathHelper.cos(theta);
+                    float x = hh;
+                    float y = r * MathHelper.sin(theta);
+                    builder.vertex(x + cx, y + cy, z + cz).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+
+            if (!isFlat)
+            {
+                for (int i = 0; i <= num_steps180; i++)
+                {
+                    builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+                    float theta = step * i;
+                    float y = r * MathHelper.cos(theta);
+
+                    float z = r * MathHelper.sin(theta);
+
+                    builder.vertex(cx + 0, cy - y, cz + z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + 0, cy + y, cz - z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + h, cy + y, cz - z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + h, cy - y, cz + z).color(red, grn, blu, alpha).next();
+                    tessellator.draw();
+                }
+            }
+            else
+            {
+                builder.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps180; i++)
+                {
+                    float theta = step * i;
+                    float y = r * MathHelper.cos(theta);
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(cx, cy - y, cz + z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx, cy + y, cz - z).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+        }
+        else if (axis == Direction.Axis.Z)
+        {
+            for (int dh = 0; dh < hsteps; dh++)
+            {
+                float hh = dh * hstep;
+                builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float y = r * MathHelper.cos(theta);
+                    float z = hh;
+                    float x = r * MathHelper.sin(theta);
+                    builder.vertex(x + cx, y + cy, z + cz).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+            if (!isFlat)
+            {
+                for (int i = 0; i <= num_steps180; i++)
+                {
+                    builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+
+                    float y = r * MathHelper.sin(theta);
+
+                    builder.vertex(cx + x, cy - y, cz + 0).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx - x, cy + y, cz + 0).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx - x, cy + y, cz + h).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + x, cy - y, cz + h).color(red, grn, blu, alpha).next();
+                    tessellator.draw();
+                }
+            }
+            else
+            {
+                builder.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps180; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float y = r * MathHelper.sin(theta);
+                    builder.vertex(cx + x, cy - y, cz).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx - x, cy + y, cz).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+
+        }
+    }
+
+    public static void drawCylinderFaces(Tessellator tessellator, BufferBuilder builder,
+                                             float cx, float cy, float cz,
+                                             float r, float h, Direction.Axis axis,  int subd, boolean isFlat,
+                                             float red, float grn, float blu, float alpha)
+    {
+        float step = (float)Math.PI / (subd/2);
+        int num_steps180 = (int)(Math.PI / step)+1;
+        int num_steps360 = (int)(2*Math.PI / step)+1;
+
+        if (axis == Direction.Axis.Y)
+        {
+
+            builder.begin(GL11.GL_TRIANGLE_FAN, VertexFormats.POSITION_COLOR);
+            builder.vertex(cx, cy, cz).color(red, grn, blu, alpha).next();
+            for (int i = 0; i <= num_steps360; i++)
+            {
+                float theta = step * i;
+                float x = r * MathHelper.cos(theta);
+                float z = r * MathHelper.sin(theta);
+                builder.vertex(x + cx, cy, z + cz).color(red, grn, blu, alpha).next();
+            }
+            tessellator.draw();
+            if (!isFlat)
+            {
+                builder.begin(GL11.GL_TRIANGLE_FAN, VertexFormats.POSITION_COLOR);
+                builder.vertex(cx, cy+h, cz).color(red, grn, blu, alpha).next();
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(x + cx, cy+h, z + cz).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+
+                builder.begin(GL11.GL_QUAD_STRIP, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(cx + x, cy + 0, cz + z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + x, cy + h, cz + z).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+
+        }
+        else if (axis == Direction.Axis.X)
+        {
+            builder.begin(GL11.GL_TRIANGLE_FAN, VertexFormats.POSITION_COLOR);
+            builder.vertex(cx, cy, cz).color(red, grn, blu, alpha).next();
+            for (int i = 0; i <= num_steps360; i++)
+            {
+                float theta = step * i;
+                float y = r * MathHelper.cos(theta);
+                float z = r * MathHelper.sin(theta);
+                builder.vertex(cx, cy + y, z + cz).color(red, grn, blu, alpha).next();
+            }
+            tessellator.draw();
+            if (!isFlat)
+            {
+                builder.begin(GL11.GL_TRIANGLE_FAN, VertexFormats.POSITION_COLOR);
+                builder.vertex(cx+h, cy, cz).color(red, grn, blu, alpha).next();
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float y = r * MathHelper.cos(theta);
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(cx+h, cy + y, cz + z).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+
+                builder.begin(GL11.GL_QUAD_STRIP, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float y = r * MathHelper.cos(theta);
+                    float z = r * MathHelper.sin(theta);
+                    builder.vertex(cx + 0, cy + y, cz + z).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + h, cy + y, cz + z).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+        }
+        else if (axis == Direction.Axis.Z)
+        {
+            builder.begin(GL11.GL_TRIANGLE_FAN, VertexFormats.POSITION_COLOR);
+            builder.vertex(cx, cy, cz).color(red, grn, blu, alpha).next();
+            for (int i = 0; i <= num_steps360; i++)
+            {
+                float theta = step * i;
+                float x = r * MathHelper.cos(theta);
+                float y = r * MathHelper.sin(theta);
+                builder.vertex(x + cx, cy+y, cz).color(red, grn, blu, alpha).next();
+            }
+            tessellator.draw();
+            if (!isFlat)
+            {
+                builder.begin(GL11.GL_TRIANGLE_FAN, VertexFormats.POSITION_COLOR);
+                builder.vertex(cx, cy, cz+h).color(red, grn, blu, alpha).next();
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float y = r * MathHelper.sin(theta);
+                    builder.vertex(x + cx, cy+y, cz+h).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+
+                builder.begin(GL11.GL_QUAD_STRIP, VertexFormats.POSITION_COLOR);
+                for (int i = 0; i <= num_steps360; i++)
+                {
+                    float theta = step * i;
+                    float x = r * MathHelper.cos(theta);
+                    float y = r * MathHelper.sin(theta);
+                    builder.vertex(cx + x, cy + y, cz + 0).color(red, grn, blu, alpha).next();
+                    builder.vertex(cx + x, cy + y, cz + h).color(red, grn, blu, alpha).next();
+                }
+                tessellator.draw();
+            }
+        }
+    }
+
+    public static void drawSphereWireframe(Tessellator tessellator, BufferBuilder builder,
+                                           float cx, float cy, float cz,
+                                           float r, int subd,
+                                           float red, float grn, float blu, float alpha)
+    {
+        float step = (float)Math.PI / (subd/2);
+        int num_steps180 = (int)(Math.PI / step)+1;
+        int num_steps360 = (int)(2*Math.PI / step);
+        for (int i = 0; i <= num_steps360; i++)
+        {
+            builder.begin(GL11.GL_LINE_STRIP, VertexFormats.POSITION_COLOR);
+            float theta = step * i ;
+            for (int j = 0; j <= num_steps180; j++)
+            {
+                float phi = step * j ;
+                float x = r * MathHelper.sin(phi) * MathHelper.cos(theta);
+                float z = r * MathHelper.sin(phi) * MathHelper.sin(theta);
+                float y = r * MathHelper.cos(phi);
+                builder.vertex(x+cx, y+cy, z+cz).color(red, grn, blu, alpha).next();
+            }
+            tessellator.draw();
+        }
+        for (int j = 0; j <= num_steps180; j++)
+        {
+            builder.begin(GL11.GL_LINE_LOOP, VertexFormats.POSITION_COLOR);
+            float phi = step * j ;
+
+            for (int i = 0; i <= num_steps360; i++)
+            {
+                float theta = step * i;
+                float x = r * MathHelper.sin(phi) * MathHelper.cos(theta);
+                float z = r * MathHelper.sin(phi) * MathHelper.sin(theta);
+                float y = r * MathHelper.cos(phi);
+                builder.vertex(x+cx, y+cy, z+cz).color(red, grn, blu, alpha).next();
+            }
+            tessellator.draw();
+        }
+
+    }
+
+    public static void drawSphereFaces(Tessellator tessellator, BufferBuilder builder,
+                                           float cx, float cy, float cz,
+                                           float r, int subd,
+                                           float red, float grn, float blu, float alpha)
+    {
+
+        float step = (float)Math.PI / (subd/2);
+        int num_steps180 = (int)(Math.PI / step)+1;
+        int num_steps360 = (int)(2*Math.PI / step);
+        for (int i = 0; i <= num_steps360; i++)
+        {
+            float theta = i * step;
+            float thetaprime = theta+step;
+            builder.begin(GL11.GL_QUAD_STRIP, VertexFormats.POSITION_COLOR);
+            for (int j = 0; j <= num_steps180; j++)
+            {
+                float phi = j * step;
+                float x = r * MathHelper.sin(phi) * MathHelper.cos(theta);
+                float z = r * MathHelper.sin(phi) * MathHelper.sin(theta);
+                float y = r * MathHelper.cos(phi);
+                float xp = r * MathHelper.sin(phi) * MathHelper.cos(thetaprime);
+                float zp = r * MathHelper.sin(phi) * MathHelper.sin(thetaprime);
+                builder.vertex(x + cx, y + cy, z + cz).color(red, grn, blu, alpha).next();
+                builder.vertex(xp + cx, y + cy, zp + cz).color(red, grn, blu, alpha).next();
+            }
+            tessellator.draw();
+        }
+    }
+}
diff --git a/src/main/java/carpet/script/value/BlockValue.java b/src/main/java/carpet/script/value/BlockValue.java
index 860b5c24..0f921b01 100644
--- a/src/main/java/carpet/script/value/BlockValue.java
+++ b/src/main/java/carpet/script/value/BlockValue.java
@@ -72,172 +72,11 @@ public class BlockValue extends Value
         throw new InternalExpressionException("Cannot parse block: "+str);
     }
 
-    public static VectorLocator locateVec(CarpetContext c, List params, int offset)
-    {
-        return locateVec(c,params, offset, false);
-    }
-    public static VectorLocator locateVec(CarpetContext c, List params, int offset, boolean optionalDirection)
-    {
-        try
-        {
-            Value v1 = params.get(0 + offset).evalValue(c);
-            if (v1 instanceof BlockValue)
-            {
-                return (new VectorLocator(new Vec3d(((BlockValue) v1).getPos()).add(0.5,0.5,0.5), 1+offset)).fromBlock();
-            }
-            if (v1 instanceof ListValue)
-            {
-                List args = ((ListValue) v1).getItems();
-                Vec3d pos = new Vec3d(
-                        NumericValue.asNumber(args.get(0)).getDouble(),
-                        NumericValue.asNumber(args.get(1)).getDouble(),
-                        NumericValue.asNumber(args.get(2)).getDouble());
-                double yaw = 0.0D;
-                double pitch = 0.0D;
-                if (args.size()>3 && optionalDirection)
-                {
-                    yaw = NumericValue.asNumber(args.get(3)).getDouble();
-                    pitch = NumericValue.asNumber(args.get(4)).getDouble();
-                }
-                return new VectorLocator(pos,offset+1, yaw, pitch);
-            }
-            Vec3d pos = new Vec3d(
-                    NumericValue.asNumber(v1).getDouble(),
-                    NumericValue.asNumber(params.get(1 + offset).evalValue(c)).getDouble(),
-                    NumericValue.asNumber(params.get(2 + offset).evalValue(c)).getDouble());
-            double yaw = 0.0D;
-            double pitch = 0.0D;
-            int eatenLength = 3;
-            if (params.size()>3+offset && optionalDirection)
-            {
-                yaw = NumericValue.asNumber(params.get(3 + offset).evalValue(c)).getDouble();
-                pitch = NumericValue.asNumber(params.get(4 + offset).evalValue(c)).getDouble();
-                eatenLength = 5;
-            }
-
-            return new VectorLocator(pos,offset+eatenLength, yaw, pitch);
-        }
-        catch (IndexOutOfBoundsException e)
-        {
-            throw new InternalExpressionException("Position should be defined either by three coordinates, or a block value");
-        }
-    }
-
     public static BlockPos locateBlockPos(CarpetContext c, int xpos, int ypos, int zpos)
     {
         return new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos);
     }
 
-    public static LocatorResult fromParams(CarpetContext c, List params, int offset)
-    {
-        return fromParams(c, params,offset, false, false);
-    }
-
-    public static LocatorResult fromParams(CarpetContext c, List params, int offset, boolean acceptString)
-    {
-        return fromParams(c, params,offset, acceptString, false);
-    }
-
-    public static LocatorResult fromParams(CarpetContext c, List params, int offset, boolean acceptString, boolean optional)
-    {
-        try
-        {
-            Value v1 = params.get(0 + offset).evalValue(c);
-            //add conditional from string name
-            if (optional && v1 instanceof NullValue)
-            {
-                return new LocatorResult(null, 1+offset);
-            }
-            if (acceptString && v1 instanceof StringValue)
-            {
-                return new LocatorResult(fromString(v1.getString()), 1+offset);
-            }
-            if (v1 instanceof BlockValue)
-            {
-                return new LocatorResult(((BlockValue) v1), 1+offset);
-            }
-            if (v1 instanceof ListValue)
-            {
-                List args = ((ListValue) v1).getItems();
-                int xpos = (int) NumericValue.asNumber(args.get(0)).getLong();
-                int ypos = (int) NumericValue.asNumber(args.get(1)).getLong();
-                int zpos = (int) NumericValue.asNumber(args.get(2)).getLong();
-                return new LocatorResult(
-                        new BlockValue(
-                                null,
-                                c.s.getWorld(),
-                                new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
-                        ),
-                        1+offset);
-            }
-            int xpos = (int) NumericValue.asNumber(v1).getLong();
-            int ypos = (int) NumericValue.asNumber( params.get(1 + offset).evalValue(c)).getLong();
-            int zpos = (int) NumericValue.asNumber( params.get(2 + offset).evalValue(c)).getLong();
-            return new LocatorResult(
-                    new BlockValue(
-                            null,
-                            c.s.getWorld(),
-                            new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
-                    ),
-                    3+offset
-            );
-        }
-        catch (IndexOutOfBoundsException e)
-        {
-            throw new InternalExpressionException("Block should be defined either by three coordinates, a block value, or a proper string");
-        }
-    }
-
-    public static LocatorResult fromParamValues(CarpetContext c, List params, int offset, boolean acceptString, boolean optional)
-    {
-        try
-        {
-            Value v1 = params.get(0 + offset);
-            //add conditional from string name
-            if (optional && v1 instanceof NullValue)
-            {
-                return new LocatorResult(null, 1+offset);
-            }
-            if (acceptString && v1 instanceof StringValue)
-            {
-                return new LocatorResult(fromString(v1.getString()), 1+offset);
-            }
-            if (v1 instanceof BlockValue)
-            {
-                return new LocatorResult(((BlockValue) v1), 1+offset);
-            }
-            if (v1 instanceof ListValue)
-            {
-                List args = ((ListValue) v1).getItems();
-                int xpos = (int) NumericValue.asNumber(args.get(0)).getLong();
-                int ypos = (int) NumericValue.asNumber(args.get(1)).getLong();
-                int zpos = (int) NumericValue.asNumber(args.get(2)).getLong();
-                return new LocatorResult(
-                        new BlockValue(
-                                null,
-                                c.s.getWorld(),
-                                new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
-                        ),
-                        1+offset);
-            }
-            int xpos = (int) NumericValue.asNumber(v1).getLong();
-            int ypos = (int) NumericValue.asNumber( params.get(1 + offset)).getLong();
-            int zpos = (int) NumericValue.asNumber( params.get(2 + offset)).getLong();
-            return new LocatorResult(
-                    new BlockValue(
-                            null,
-                            c.s.getWorld(),
-                            new BlockPos(c.origin.getX() + xpos, c.origin.getY() + ypos, c.origin.getZ() + zpos)
-                    ),
-                    3+offset
-            );
-        }
-        catch (IndexOutOfBoundsException e)
-        {
-            throw new InternalExpressionException("Block should be defined either by three coordinates, a block value, or a proper string");
-        }
-    }
-
     public BlockState getBlockState()
     {
         if (blockState != null)
@@ -342,46 +181,6 @@ public class BlockValue extends Value
     public ServerWorld getWorld() { return world;}
 
 
-    public static class LocatorResult
-    {
-        public final BlockValue block;
-        public final int offset;
-        LocatorResult(BlockValue b, int o)
-        {
-            block = b;
-            offset = o;
-        }
-    }
-
-    public static class VectorLocator
-    {
-        public Vec3d vec;
-        public final int offset;
-        public final double yaw;
-        public final double pitch;
-        public boolean fromBlock;
-        VectorLocator(Vec3d v, int o)
-        {
-            vec = v;
-            offset = o;
-            yaw = 0.0D;
-            pitch = 0.0D;
-        }
-        VectorLocator(Vec3d v, int o, double y, double p)
-        {
-            vec = v;
-            offset = o;
-            yaw = y;
-            pitch = p;
-        }
-
-        public VectorLocator fromBlock()
-        {
-            fromBlock = true;
-            return this;
-        }
-    }
-
     public enum SpecificDirection {
         UP("up",0.5, 0.0, 0.5, Direction.UP),
 
diff --git a/src/main/java/carpet/script/value/EntityValue.java b/src/main/java/carpet/script/value/EntityValue.java
index 3cd5726e..0b053f2c 100644
--- a/src/main/java/carpet/script/value/EntityValue.java
+++ b/src/main/java/carpet/script/value/EntityValue.java
@@ -1,15 +1,21 @@
 package carpet.script.value;
 
+import carpet.CarpetSettings;
 import carpet.fakes.EntityInterface;
 import carpet.fakes.ItemEntityInterface;
+import carpet.fakes.LivingEntityInterface;
 import carpet.fakes.MobEntityInterface;
+import carpet.fakes.HungerManagerInterface;
 import carpet.helpers.Tracer;
 import carpet.patches.EntityPlayerMPFake;
 import carpet.script.CarpetContext;
 import carpet.script.EntityEventsGroup;
+import carpet.script.argument.Vector3Argument;
 import carpet.script.exception.InternalExpressionException;
+import carpet.utils.Messenger;
 import com.mojang.brigadier.StringReader;
 import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import net.minecraft.entity.player.HungerManager;
 import net.minecraft.network.packet.s2c.play.EntityPassengersSetS2CPacket;
 import net.minecraft.network.packet.s2c.play.EntityPositionS2CPacket;
 import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket;
@@ -45,6 +51,7 @@ import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.MathHelper;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.util.registry.Registry;
+import net.minecraft.world.GameMode;
 import org.apache.commons.lang3.tuple.Pair;
 
 import java.util.ArrayList;
@@ -60,6 +67,7 @@ import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import static carpet.script.value.NBTSerializableValue.nameFromRegistryId;
+import static carpet.utils.MobAI.genericJump;
 
 // TODO: decide whether copy(entity) should duplicate entity in the world.
 public class EntityValue extends Value
@@ -118,7 +126,7 @@ public class EntityValue extends Value
     @Override
     public String getString()
     {
-        return entity.getDisplayName().getString();
+        return entity.getName().getString();
     }
 
     @Override
@@ -220,7 +228,9 @@ public class EntityValue extends Value
         put("motion_x", (e, a) -> new NumericValue(e.getVelocity().x));
         put("motion_y", (e, a) -> new NumericValue(e.getVelocity().y));
         put("motion_z", (e, a) -> new NumericValue(e.getVelocity().z));
-        put("name", (e, a) -> new StringValue(e.getDisplayName().getString()));
+        put("name", (e, a) -> new StringValue(e.getName().getString()));
+        put("display_name", (e, a) -> new StringValue(e.getDisplayName().getString()));
+        put("command_name", (e, a) -> new StringValue(e.getEntityName()));
         put("custom_name", (e, a) -> e.hasCustomName()?new StringValue(e.getCustomName().getString()):Value.NULL);
         put("type", (e, a) -> new StringValue(nameFromRegistryId(Registry.ENTITY_TYPE.getId(e.getType()))));
         put("is_riding", (e, a) -> new NumericValue(e.hasVehicle()));
@@ -252,7 +262,9 @@ public class EntityValue extends Value
         put("despawn_timer", (e, a) -> e instanceof LivingEntity?new NumericValue(((LivingEntity) e).getDespawnCounter()):Value.NULL);
         put("item", (e, a) -> (e instanceof ItemEntity)?ListValue.fromItemStack(((ItemEntity) e).getStack()):Value.NULL);
         put("count", (e, a) -> (e instanceof ItemEntity)?new NumericValue(((ItemEntity) e).getStack().getCount()):Value.NULL);
-        put("pickup_delay", (e, a) -> (e instanceof ItemEntity)?new NumericValue(((ItemEntityInterface) e).getPickupDelay()):Value.NULL);
+        put("pickup_delay", (e, a) -> (e instanceof ItemEntity)?new NumericValue(((ItemEntityInterface) e).getPickupDelayCM()):Value.NULL);
+        put("portal_cooldown", (e , a) ->new NumericValue(((EntityInterface)e).getPortalTimer()));
+        put("portal_timer", (e , a) ->new NumericValue(e.netherPortalCooldown));
         // ItemEntity -> despawn timer via ssGetAge
         put("is_baby", (e, a) -> (e instanceof LivingEntity)?new NumericValue(((LivingEntity) e).isBaby()):Value.NULL);
         put("target", (e, a) -> {
@@ -277,13 +289,27 @@ public class EntityValue extends Value
         put("sneaking", (e, a) -> e.isSneaking()?Value.TRUE:Value.FALSE);
         put("sprinting", (e, a) -> e.isSprinting()?Value.TRUE:Value.FALSE);
         put("swimming", (e, a) -> e.isSwimming()?Value.TRUE:Value.FALSE);
-        /*put("jumping", (e, a) -> {
+        put("hunger", (e, a) -> {
+            if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).getHungerManager().getFoodLevel());
+            return Value.NULL;
+        });
+        put("saturation", (e, a) -> {
+            if(e instanceof PlayerEntity) return new NumericValue(((PlayerEntity) e).getHungerManager().getSaturationLevel());
+            return Value.NULL;
+        });
+
+        put("exhaustion",(e, a)->{
+            if(e instanceof PlayerEntity) return new NumericValue(((HungerManagerInterface)((PlayerEntity) e).getHungerManager()).getExhaustionCM());
+            return Value.NULL;
+        });
+
+        put("jumping", (e, a) -> {
             if (e instanceof LivingEntity)
             {
-                return  ((LivingEntity) e).getJumping()?Value.TRUE:Value.FALSE;
+                return  ((LivingEntityInterface) e).isJumpingCM()?Value.TRUE:Value.FALSE;
             }
             return Value.NULL;
-        });*/ //needs mixing
+        });
         put("gamemode", (e, a) -> {
             if (e instanceof  ServerPlayerEntity)
             {
@@ -331,6 +357,8 @@ public class EntityValue extends Value
             return Value.NULL;
         });
 
+        put("team", (e, a) -> e.getScoreboardTeam()==null?Value.NULL:new StringValue(e.getScoreboardTeam().getName()));
+
         put("ping", (e, a) -> {
             if (e instanceof  ServerPlayerEntity)
             {
@@ -370,6 +398,7 @@ public class EntityValue extends Value
             StatusEffectInstance pe = ((LivingEntity) e).getStatusEffect(potion);
             return ListValue.of( new NumericValue(pe.getAmplifier()), new NumericValue(pe.getDuration()) );
         });
+
         put("health", (e, a) ->
         {
             if (e instanceof LivingEntity)
@@ -475,6 +504,8 @@ public class EntityValue extends Value
                 return new NBTSerializableValue(nbttagcompound);
             return new NBTSerializableValue(nbttagcompound).get(a);
         });
+
+        put("category",(e,a)->{return new StringValue(e.getType().getCategory().toString().toLowerCase(Locale.ROOT));});
     }};
 
     public void set(String what, Value toWhat)
@@ -523,12 +554,40 @@ public class EntityValue extends Value
         //((ServerWorld)e.getEntityWorld()).method_14178().sendToNearbyPlayers(e, new EntityVelocityUpdateS2CPacket(e));
     }
 
-
-
     private static final Map> featureModifiers = new HashMap>() {{
         put("remove", (entity, value) -> entity.remove());
         put("age", (e, v) -> e.age = Math.abs((int)NumericValue.asNumber(v).getLong()) );
-        put("health", (e, v) -> { if (e instanceof LivingEntity) ((LivingEntity) e).setHealth((float) NumericValue.asNumber(v).getDouble()); });
+        put("health", (e, v) -> {
+            float health = (float) NumericValue.asNumber(v).getDouble();
+            if (health <= 0f && e instanceof ServerPlayerEntity)
+            {
+                ServerPlayerEntity player = (ServerPlayerEntity) e;
+                if (player.container != null)
+                {
+                    // if player dies with open container, then that causes NPE on the client side
+                    // its a client side bug that may never surface unless vanilla gets into scripting at some point
+                    // bug: #228
+                    player.closeContainer();
+                }
+                ((LivingEntity) e).setHealth(health);
+            }
+            if (e instanceof LivingEntity) ((LivingEntity) e).setHealth(health);
+        });
+        // todo add handling of the source for extra effects
+        /*put("damage", (e, v) -> {
+            float dmgPoints;
+            DamageSource source;
+            if (v instanceof ListValue && ((ListValue) v).getItems().size() > 1)
+            {
+                   List vals = ((ListValue) v).getItems();
+                   dmgPoints = (float) NumericValue.asNumber(v).getDouble();
+                   source = DamageSource ... yeah...
+            }
+            else
+            {
+
+            }
+        });*/
         put("kill", (e, v) -> e.kill());
         put("location", (e, v) ->
         {
@@ -751,26 +810,12 @@ public class EntityValue extends Value
             else if (v instanceof ListValue)
             {
                 List lv = ((ListValue) v).getItems();
-                if (lv.get(0) instanceof BlockValue)
+                Vector3Argument locator = Vector3Argument.findIn(lv, 0, false);
+                pos = new BlockPos(locator.vec.x, locator.vec.y, locator.vec.z);
+                if (lv.size() > locator.offset)
                 {
-                    pos = ((BlockValue) lv.get(0)).getPos();
-                    if (lv.size()>1)
-                    {
-                        distance = (int) NumericValue.asNumber(lv.get(1)).getLong();
-                    }
+                    distance = (int) NumericValue.asNumber(lv.get(locator.offset)).getLong();
                 }
-                else if (lv.size()>=3)
-                {
-                    pos = new BlockPos(NumericValue.asNumber(lv.get(0)).getLong(),
-                            NumericValue.asNumber(lv.get(1)).getLong(),
-                            NumericValue.asNumber(lv.get(2)).getLong());
-                    if (lv.size()>3)
-                    {
-                        distance = (int) NumericValue.asNumber(lv.get(4)).getLong();
-                    }
-                }
-                else throw new InternalExpressionException("'home' requires at least one position argument, and optional distance");
-
             }
             else throw new InternalExpressionException("'home' requires at least one position argument, and optional distance");
 
@@ -800,6 +845,20 @@ public class EntityValue extends Value
             }
         });
 
+        put("portal_cooldown", (e , v) ->
+        {
+            if (v==null)
+                throw new InternalExpressionException("'portal_cooldown' requires a value to set");
+            e.netherPortalCooldown = NumericValue.asNumber(v).getInt();
+        });
+
+        put("portal_timer", (e , v) ->
+        {
+            if (v==null)
+                throw new InternalExpressionException("'portal_timer' requires a value to set");
+            ((EntityInterface) e).setPortalTimer(NumericValue.asNumber(v).getInt());
+        });
+
         put("ai", (e, v) ->
         {
             if (e instanceof MobEntity)
@@ -820,7 +879,10 @@ public class EntityValue extends Value
             if (!(e instanceof LivingEntity)) return;
             LivingEntity le = (LivingEntity)e;
             if (v == null)
+            {
                 le.clearStatusEffects();
+                return;
+            }
             else if (v instanceof ListValue)
             {
                 List lv = ((ListValue) v).getItems();
@@ -840,7 +902,7 @@ public class EntityValue extends Value
                     {
                         le.removeStatusEffect(effect);
                         return;
-                    } 
+                    }
                     int amplifier = 0;
                     if (lv.size() > 2)
                         amplifier = (int)NumericValue.asNumber(lv.get(2)).getLong();
@@ -854,24 +916,81 @@ public class EntityValue extends Value
                     return;
                 }
             }
+            else
+            {
+                String effectName = v.getString();
+                StatusEffect effect = Registry.STATUS_EFFECT.get(new Identifier(effectName));
+                if (effect == null)
+                    throw new InternalExpressionException("Wrong effect name: "+effectName);
+                le.removeStatusEffect(effect);
+                return;
+            }
             throw new InternalExpressionException("'effect' needs either no arguments (clear) or effect name, duration, and optional amplifier, show particles and show icon");
         });
 
-        // gamemode
-        // spectate
-        // "fire"
-        // "extinguish"
-        // "silent"
-        // "gravity"
-        // "invulnerable"
-        // "dimension"
-        // "item"
-        // "count",
-        // "age",
-        // "effect_"name
-        // "hold"
-        // "hold_offhand"
-        // "jump"
+        put("gamemode", (e,v)->{
+            if(!(e instanceof ServerPlayerEntity)) return;
+            GameMode toSet = v instanceof NumericValue ?
+                    GameMode.byId(((NumericValue) v).getInt(), null) :
+                    GameMode.byName(v.getString().toLowerCase(Locale.ROOT), null);
+            if (toSet != null) ((ServerPlayerEntity) e).setGameMode(toSet);
+        });
+
+        put("jumping",(e,v)->{
+            if(!(e instanceof LivingEntity)) return;
+            ((LivingEntity) e).setJumping(v.getBoolean());
+        });
+
+        put("jump",(e,v)->{
+            if (e instanceof LivingEntity)
+            {
+                ((LivingEntityInterface)e).doJumpCM();
+            }
+            else
+            {
+                genericJump(e);
+            }
+        });
+
+        put("silent",(e,v)-> e.setSilent(v.getBoolean()));
+
+        put("gravity",(e,v)-> e.setNoGravity(!v.getBoolean()));
+
+        put("invulnerable",(e,v)-> e.setInvulnerable(v.getBoolean()));
+
+        put("fire",(e,v)-> e.setFireTicks((int)NumericValue.asNumber(v).getLong()));
+
+        put("hunger", (e, v)-> {
+            if(e instanceof PlayerEntity) ((PlayerEntity) e).getHungerManager().setFoodLevel((int) NumericValue.asNumber(v).getLong());
+        });
+
+        put("exhaustion", (e, v)-> {
+            if(e instanceof PlayerEntity) ((HungerManagerInterface) ((PlayerEntity) e).getHungerManager()).setExhaustionCM(NumericValue.asNumber(v).getFloat());
+        });
+
+        put("add_exhaustion", (e, v)-> {
+            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());
+        });
+
+        // gamemode         [check]
+        // spectate         [check]
+        // "fire"           [check]
+        // "extinguish"     [set fire ticks to 0]
+        // "silent"         [check]
+        // "gravity"        [check]
+        // "invulnerable"   [check]
+        // "dimension"      []
+        // "item"           []
+        // "count",         []
+        // "age",           [check]
+        // "effect_"name    []
+        // "hold"           [inventory_set?]
+        // "hold_offhand"   [inventory_set?] 
+        // "jump"           [check]
         // "nbt" <-big one, for now use run('data merge entity ...
     }};
 
diff --git a/src/main/java/carpet/script/value/FormattedTextValue.java b/src/main/java/carpet/script/value/FormattedTextValue.java
new file mode 100644
index 00000000..501e3181
--- /dev/null
+++ b/src/main/java/carpet/script/value/FormattedTextValue.java
@@ -0,0 +1,40 @@
+package carpet.script.value;
+
+import net.minecraft.text.BaseText;
+
+public class FormattedTextValue extends StringValue
+{
+    BaseText text;
+    public FormattedTextValue(BaseText text)
+    {
+        super(null);
+        this.text = text;
+    }
+
+    @Override
+    public String getString() {
+        return text.getString();
+    }
+
+    @Override
+    public boolean getBoolean() {
+          return text.getSiblings().size() > 0;
+    }
+
+    @Override
+    public Value clone()
+    {
+        return new FormattedTextValue(text);
+    }
+
+    @Override
+    public String getTypeString()
+    {
+        return "text";
+    }
+
+    public BaseText getText()
+    {
+        return text;
+    }
+}
diff --git a/src/main/java/carpet/script/value/FunctionValue.java b/src/main/java/carpet/script/value/FunctionValue.java
index 14f6498d..3b872ccf 100644
--- a/src/main/java/carpet/script/value/FunctionValue.java
+++ b/src/main/java/carpet/script/value/FunctionValue.java
@@ -5,6 +5,7 @@ import carpet.script.Expression;
 import carpet.script.Fluff;
 import carpet.script.LazyValue;
 import carpet.script.Tokenizer;
+import carpet.script.bundled.Module;
 import carpet.script.exception.BreakStatement;
 import carpet.script.exception.ContinueStatement;
 import carpet.script.exception.ExitStatement;
@@ -58,6 +59,8 @@ public class FunctionValue extends Value implements Fluff.ILazyFunction
         return name;
     }
 
+    public Module getModule() {return expression.module;}
+
     @Override
     public String getPrettyString()
     {
diff --git a/src/main/java/carpet/script/value/LazyListValue.java b/src/main/java/carpet/script/value/LazyListValue.java
index 55df4ae5..e57d7110 100644
--- a/src/main/java/carpet/script/value/LazyListValue.java
+++ b/src/main/java/carpet/script/value/LazyListValue.java
@@ -48,7 +48,7 @@ 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, "[%d, %d, ..., %d)",start, start+stepp, limit);
             }
         };
     }
diff --git a/src/main/java/carpet/script/value/ListValue.java b/src/main/java/carpet/script/value/ListValue.java
index 2d1ac5dc..e20d32b3 100644
--- a/src/main/java/carpet/script/value/ListValue.java
+++ b/src/main/java/carpet/script/value/ListValue.java
@@ -75,6 +75,18 @@ public class ListValue extends AbstractListValue implements ContainerValueInterf
         );
     }
 
+    public static Value fromTriple(double a, double b, double c)
+    {
+        return ListValue.of(new NumericValue(a), new NumericValue(b), new NumericValue(c));
+    }
+
+    public static Value fromTriple(int a, int b, int c)
+    {
+        return fromTriple((double) a, b, c);
+    }
+
+
+
 
     public static ListValue wrap(List list)
     {
diff --git a/src/main/java/carpet/script/value/MapValue.java b/src/main/java/carpet/script/value/MapValue.java
index 8e22001c..61965e4e 100644
--- a/src/main/java/carpet/script/value/MapValue.java
+++ b/src/main/java/carpet/script/value/MapValue.java
@@ -7,6 +7,7 @@ import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 public class MapValue extends AbstractListValue implements ContainerValueInterface
@@ -27,6 +28,15 @@ public class MapValue extends AbstractListValue implements ContainerValueInterfa
         }
     }
 
+    public MapValue(Set keySet)
+    {
+        this();
+        for (Value v : keySet)
+        {
+            map.put(v, Value.NULL);
+        }
+    }
+
     @Override
     public Iterator iterator()
     {
diff --git a/src/main/java/carpet/script/value/NBTSerializableValue.java b/src/main/java/carpet/script/value/NBTSerializableValue.java
index 07256909..b4ce70b5 100644
--- a/src/main/java/carpet/script/value/NBTSerializableValue.java
+++ b/src/main/java/carpet/script/value/NBTSerializableValue.java
@@ -27,6 +27,7 @@ import net.minecraft.nbt.StringNbtReader;
 import net.minecraft.nbt.StringTag;
 import net.minecraft.nbt.Tag;
 import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.server.world.ServerWorld;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
@@ -35,6 +36,7 @@ import net.minecraft.util.math.Box;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
@@ -201,16 +203,30 @@ public class NBTSerializableValue extends Value implements ContainerValueInterfa
                     return null;
                 return new InventoryLocator(pos, pos, inv, offset + 1);
             }
-            else if (v1.getString().equalsIgnoreCase("enderchest"))
+            else if (v1 instanceof StringValue)
             {
-                Value v2 = params.get(1 + offset).evalValue(c);
-                if (!(v2 instanceof EntityValue) || !(((EntityValue) v2).getEntity() instanceof PlayerEntity))
+                String strVal = v1.getString().toLowerCase(Locale.ROOT);
+                if (strVal.equals("enderchest"))
                 {
-                    throw new InternalExpressionException("enderchest inventory requires player argument");
+                    Value v2 = params.get(1 + offset).evalValue(c);
+                    ServerPlayerEntity player = EntityValue.getPlayerByValue(c.s.getMinecraftServer(), v2);
+                    if (player == null) throw new InternalExpressionException("enderchest inventory requires player argument");
+                    return new InventoryLocator(player, player.getBlockPos(), player.getEnderChestInventory(), offset + 2, true);
+                }
+                else
+                {
+                    boolean isEnder = strVal.startsWith("enderchest_");
+                    if (isEnder) strVal = strVal.substring(11); // len("enderchest_")
+                    ServerPlayerEntity player = c.s.getMinecraftServer().getPlayerManager().getPlayer(strVal);
+                    if (player == null) throw new InternalExpressionException("String description of an inventory should either denote a player or player's enderchest");
+                    return new InventoryLocator(
+                            player,
+                            player.getBlockPos(),
+                            isEnder ? player.getEnderChestInventory() : player.inventory,
+                            offset + 1,
+                            isEnder
+                    );
                 }
-                PlayerEntity e = (PlayerEntity)((EntityValue) v2).getEntity();
-                inv = e.getEnderChestInventory();
-                return new InventoryLocator(e, e.getBlockPos(), inv, offset + 2, true);
             }
             BlockPos pos = new BlockPos(
                     NumericValue.asNumber(v1).getDouble(),
diff --git a/src/main/java/carpet/script/value/NumericValue.java b/src/main/java/carpet/script/value/NumericValue.java
index 087d0f2e..2ae575fb 100644
--- a/src/main/java/carpet/script/value/NumericValue.java
+++ b/src/main/java/carpet/script/value/NumericValue.java
@@ -13,6 +13,13 @@ public class NumericValue extends Value
     private Double value;
     final static double epsilon = 1024*Double.MIN_VALUE;
 
+    public static NumericValue asNumber(Value v1, String id)
+    {
+        if (!(v1 instanceof NumericValue))
+            throw new InternalExpressionException("Argument "+id+" has to be of a numeric type");
+        return ((NumericValue) v1);
+    }
+
     public static NumericValue asNumber(Value v1)
     {
         if (!(v1 instanceof NumericValue))
@@ -56,6 +63,10 @@ public class NumericValue extends Value
     {
         return value;
     }
+    public float getFloat()
+    {
+        return value.floatValue();
+    }
 
     public static long floor(double double_1) {
         long int_1 = (long)double_1;
diff --git a/src/main/java/carpet/settings/ParsedRule.java b/src/main/java/carpet/settings/ParsedRule.java
index e75bb3f6..f25b0a38 100644
--- a/src/main/java/carpet/settings/ParsedRule.java
+++ b/src/main/java/carpet/settings/ParsedRule.java
@@ -1,6 +1,7 @@
 package carpet.settings;
 
 import carpet.CarpetServer;
+import carpet.utils.Translations;
 import carpet.utils.Messenger;
 import com.google.common.collect.ImmutableList;
 import net.minecraft.server.command.ServerCommandSource;
@@ -12,6 +13,8 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Locale;
 
+import static carpet.utils.Translations.tr;
+
 public final class ParsedRule implements Comparable {
     public final Field field;
     public final String name;
@@ -225,4 +228,33 @@ public final class ParsedRule implements Comparable {
     {
         return this.name + ": " + getAsString();
     }
+
+    private String translationKey()
+    {
+        return String.format("rule.%s.name", name);
+    }
+
+    public String translatedName(){
+        String key = translationKey();
+        return Translations.hasTranslation(key) ? tr(key) + String.format(" (%s)", name): name;
+    }
+
+    public String translatedDescription()
+    {
+        return tr(String.format("rule.%s.desc", (name)), description);
+    }
+
+    public List translatedExtras()
+    {
+        if (!Translations.hasTranslations()) return extraInfo;
+        String keyBase = String.format("rule.%s.extra.", name);
+        List extras = new ArrayList<>();
+        int i = 0;
+        while (Translations.hasTranslation(keyBase+i))
+        {
+            extras.add(Translations.tr(keyBase+i));
+            i++;
+        }
+        return (extras.isEmpty()) ? extraInfo : extras;
+    }
 }
diff --git a/src/main/java/carpet/settings/SettingsManager.java b/src/main/java/carpet/settings/SettingsManager.java
index 675df7b8..b792de08 100644
--- a/src/main/java/carpet/settings/SettingsManager.java
+++ b/src/main/java/carpet/settings/SettingsManager.java
@@ -2,6 +2,8 @@ package carpet.settings;
 
 import carpet.CarpetServer;
 import carpet.CarpetSettings;
+import carpet.network.ServerNetworkHandler;
+import carpet.utils.Translations;
 import carpet.utils.Messenger;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -39,6 +41,7 @@ import java.util.Set;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
+import static carpet.utils.Translations.tr;
 import static net.minecraft.server.command.CommandManager.argument;
 import static net.minecraft.server.command.CommandManager.literal;
 import static net.minecraft.server.command.CommandSource.suggestMatching;
@@ -78,7 +81,7 @@ public class SettingsManager
 
     public void detachServer()
     {
-        for (ParsedRule rule : rules.values()) rule.resetToDefault(server.getCommandSource());
+        for (ParsedRule rule : rules.values()) rule.resetToDefault(null);
         server = null;
     }
 
@@ -101,6 +104,7 @@ public class SettingsManager
     void notifyRuleChanged(ServerCommandSource source, ParsedRule rule, String userTypedValue)
     {
         observers.forEach(observer -> observer.accept(source, rule, userTypedValue));
+        ServerNetworkHandler.updateRuleWithConnectedClients(rule);
     }
 
     public Iterable getCategories()
@@ -350,7 +354,7 @@ public class SettingsManager
         String ruleName = StringArgumentType.getString(ctx, "rule");
         ParsedRule rule = getRule(ruleName);
         if (rule == null)
-            throw new SimpleCommandExceptionType(Messenger.c("rb Unknown rule: "+ruleName)).create();
+            throw new SimpleCommandExceptionType(Messenger.c("rb "+ tr("ui.unknown_rule","Unknown rule")+": "+ruleName)).create();
         return rule;
     }
 
@@ -358,26 +362,25 @@ public class SettingsManager
     {
         if (dispatcher.getRoot().getChildren().stream().anyMatch(node -> node.getName().equalsIgnoreCase(identifier)))
         {
-            Messenger.print_server_message(CarpetServer.minecraft_server,
-                    Messenger.c("rb Failed to add settings command for " + identifier + ". It is masking previous command."));
+            CarpetSettings.LOG.error("Failed to add settings command for " + identifier + ". It is masking previous command.");
             return;
         }
 
         LiteralArgumentBuilder literalargumentbuilder = literal(identifier).requires((player) ->
                 player.hasPermissionLevel(2) && !locked);
 
-        literalargumentbuilder.executes((context)->listAllSettings(context.getSource())).
+        literalargumentbuilder.executes((context)-> listAllSettings(context.getSource())).
                 then(literal("list").
-                        executes( (c) -> listSettings(c.getSource(), "All "+fancyName+" Settings",
+                        executes( (c) -> listSettings(c.getSource(), String.format(tr("ui.all_%(mod)s_settings","All %s Settings"), fancyName),
                                 getRules())).
                         then(literal("defaults").
                                 executes( (c)-> listSettings(c.getSource(),
-                                        "Current "+fancyName+" Startup Settings from "+identifier+".conf",
+                                        String.format(tr("ui.current_%(mod)s_startup_settings_from_%(conf)s","Current %s Startup Settings from %s"), fancyName, (identifier+".conf")),
                                         findStartupOverrides()))).
                         then(argument("tag",StringArgumentType.word()).
                                 suggests( (c, b)->suggestMatching(getCategories(), b)).
                                 executes( (c) -> listSettings(c.getSource(),
-                                        String.format(fancyName+" Settings matching \"%s\"", StringArgumentType.getString(c, "tag")),
+                                        String.format(tr("ui.%(mod)s_settings_matching_'%(query)s'","%s Settings matching \"%s\""), fancyName, tr("category." + StringArgumentType.getString(c, "tag"),StringArgumentType.getString(c, "tag"))),
                                         getRulesMatching(StringArgumentType.getString(c, "tag")))))).
                 then(literal("removeDefault").
                         requires(s -> !locked).
@@ -405,33 +408,35 @@ public class SettingsManager
     private int displayRuleMenu(ServerCommandSource source, ParsedRule rule)
     {
         PlayerEntity player;
+        String displayName = rule.translatedName();
         try
         {
             player = source.getPlayer();
         }
         catch (CommandSyntaxException e)
         {
-            Messenger.m(source, "w "+rule.name +" is set to: ","wb "+rule.getAsString());
+            Messenger.m(source, "w "+ displayName +" "+ tr( "ui.is_set_to","is set to")+": ","wb "+rule.getAsString());
             return 1;
         }
 
         Messenger.m(player, "");
-        Messenger.m(player, "wb "+rule.name,"!/"+identifier+" "+rule.name,"^g refresh");
-        Messenger.m(player, "w "+rule.description);
+        Messenger.m(player, "wb "+ displayName ,"!/"+identifier+" "+rule.name,"^g refresh");
+        Messenger.m(player, "w "+ rule.translatedDescription());
 
-        rule.extraInfo.forEach(s -> Messenger.m(player, "g  "+s));
+        rule.translatedExtras().forEach(s -> Messenger.m(player, "g  "+s));
 
         List tags = new ArrayList<>();
-        tags.add(Messenger.c("w Tags: "));
+        tags.add(Messenger.c("w "+ tr("ui.tags", "Tags")+": "));
         for (String t: rule.categories)
         {
-            tags.add(Messenger.c("c ["+t+"]", "^g list all "+t+" settings","!/"+identifier+" list "+t));
+            String translated = tr("category." + t, t);
+            tags.add(Messenger.c("c ["+ translated +"]", "^g "+ String.format(tr("list_all_%s_settings","list all %s settings"), translated),"!/"+identifier+" list "+t));
             tags.add(Messenger.c("w , "));
         }
         tags.remove(tags.size()-1);
         Messenger.m(player, tags.toArray(new Object[0]));
 
-        Messenger.m(player, "w Current value: ",String.format("%s %s (%s value)",rule.getBoolValue()?"lb":"nb", rule.getAsString(),rule.isDefault()?"default":"modified"));
+        Messenger.m(player, "w "+ tr("ui.current_value", "Current value")+": ",String.format("%s %s (%s value)",rule.getBoolValue()?"lb":"nb", rule.getAsString(),rule.isDefault()?"default":"modified"));
         List options = new ArrayList<>();
         options.add(Messenger.c("w Options: ", "y [ "));
         for (String o: rule.options)
@@ -449,8 +454,8 @@ public class SettingsManager
     private int setRule(ServerCommandSource source, ParsedRule rule, String newValue)
     {
         if (rule.set(source, newValue) != null)
-            Messenger.m(source, "w "+rule.toString()+", ", "c [change permanently?]",
-                    "^w Click to keep the settings in "+identifier+".conf to save across restarts",
+            Messenger.m(source, "w "+rule.toString()+", ", "c ["+ tr("ui.change_permanently","change permanently")+"?]",
+                    "^w "+String.format(tr("ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts","Click to keep the settings in %s to save across restarts"), identifier+".conf"),
                     "?/"+identifier+" setDefault "+rule.name+" "+rule.getAsString());
         return 1;
     }
@@ -465,7 +470,7 @@ public class SettingsManager
         writeSettingsToConf(conf.getLeft()); // this may feels weird, but if conf
         // is locked, it will never reach this point.
         rule.set(source,stringValue);
-        Messenger.m(source ,"gi rule "+ rule.name+" will now default to "+ stringValue);
+        Messenger.m(source ,"gi "+String.format(tr("ui.rule_%(rule)s_will_now_default_to_%(value)s","rule %s will now default to %s"), rule.translatedName(), stringValue));
         return 1;
     }
     // removes overrides of the default values in the file
@@ -482,10 +487,11 @@ public class SettingsManager
 
     private BaseText displayInteractiveSetting(ParsedRule rule)
     {
+        String displayName = rule.translatedName();
         List args = new ArrayList<>();
-        args.add("w - "+rule.name+" ");
+        args.add("w - "+ displayName +" ");
         args.add("!/"+identifier+" "+rule.name);
-        args.add("^y "+rule.description);
+        args.add("^y "+rule.translatedDescription());
         for (String option: rule.options)
         {
             args.add(makeSetRuleButton(rule, option, true));
@@ -511,10 +517,10 @@ public class SettingsManager
         }
         String baseText = style + (brackets ? " [" : " ") + option + (brackets ? "]" : "");
         if (locked)
-            return Messenger.c(baseText, "^g "+fancyName+" settings are locked");
+            return Messenger.c(baseText, "^g "+fancyName+" " + tr("ui.settings_are_locked","settings are locked"));
         if (option.equalsIgnoreCase(rule.getAsString()))
             return Messenger.c(baseText);
-        return Messenger.c(baseText, "^g Switch to " + option+(option.equalsIgnoreCase(rule.defaultAsString)?" (default)":""), "?/"+identifier+" " + rule.name + " " + option);
+        return Messenger.c(baseText, "^g "+ tr("ui.switch_to","Switch to") +" " + option+(option.equalsIgnoreCase(rule.defaultAsString)?" (default)":""), "?/"+identifier+" " + rule.name + " " + option);
     }
 
     private int listSettings(ServerCommandSource source, String title, Collection> settings_list)
@@ -535,19 +541,26 @@ public class SettingsManager
     }
     private int listAllSettings(ServerCommandSource source)
     {
-        listSettings(source, "Current "+fancyName+" Settings", getNonDefault());
+        //listSettings(source, "Current "+fancyName+" Settings", getNonDefault());
+        listSettings(source, String.format(tr("ui.current_%(mod)s_settings","Current %s Settings"), fancyName), getNonDefault());
 
         if (version != null)
-            Messenger.m(source, "g "+fancyName+" version: "+ version);
+            //Messenger.m(source, "g "+fancyName+" version: "+ version);
+            Messenger.m(source, "g "+fancyName+" "+ tr("ui.version",  "version") + ": "+ version);
         try
         {
             PlayerEntity player = source.getPlayer();
             List tags = new ArrayList<>();
-            tags.add("w Browse Categories:\n");
+            //tags.add("w Browse Categories:\n");
+            tags.add("w " + tr("ui.browse_categories", "Browse Categories")  + ":\n");
             for (String t : getCategories())
             {
-                tags.add("c [" + t+"]");
-                tags.add("^g list all " + t + " settings");
+                String catKey = "category." + t;
+                String translated = tr(catKey, t);
+                String translatedPlus = Translations.hasTranslation(catKey) ? String.format("%s (%s)",tr(catKey, t), t) : t;
+                tags.add("c [" + translated +"]");
+                //tags.add("^g list all " + translated + " settings");
+                tags.add("^g " + String.format(tr("ui.list_all_%(cat)s_settings","list all %s settings"), translatedPlus));
                 tags.add("!/"+identifier+" list " + t);
                 tags.add("w  ");
             }
@@ -558,7 +571,7 @@ public class SettingsManager
         return 1;
     }
 
-    public void inspectClientsideCommand(String string)
+    public void inspectClientsideCommand(ServerCommandSource source, String string)
     {
         if (string.startsWith("/carpet "))
         {
@@ -569,11 +582,9 @@ public class SettingsManager
                 String strOption = res[2];
                 if (rules.containsKey(setting) && rules.get(setting).isClient)
                 {
-                    rules.get(setting).set(null, strOption);
+                    rules.get(setting).set(source, strOption);
                 }
             }
-
-
         }
     }
 }
diff --git a/src/main/java/carpet/settings/Validator.java b/src/main/java/carpet/settings/Validator.java
index 56bf64a0..3f48fff0 100644
--- a/src/main/java/carpet/settings/Validator.java
+++ b/src/main/java/carpet/settings/Validator.java
@@ -17,23 +17,12 @@ public abstract class Validator
     public abstract T validate(ServerCommandSource source, ParsedRule currentRule, T newValue, String string);
     public String description() {return null;}
 
-    public static class POSITIVE_NUMBER extends Validator
-    {
-        @Override
-        public T validate(ServerCommandSource source, ParsedRule currentRule, T newValue, String string)
-        {
-            return newValue.doubleValue() > 0 ? newValue : null;
-        }
-        @Override
-        public String description() { return "Must be a positive number"; }
-    }
-
     public static class _COMMAND extends Validator
     {
         @Override
         public T validate(ServerCommandSource source, ParsedRule currentRule, T newValue, String string)
         {
-            if (CarpetServer.settingsManager != null)
+            if (CarpetServer.settingsManager != null && source != null)
                 CarpetServer.settingsManager.notifyPlayersCommandsChanged();
             return newValue;
         }
@@ -119,4 +108,15 @@ public abstract class Validator
         @Override
         public String description() { return "Must be a positive number";}
     }
+
+    public static class PROBABILITY  extends Validator
+    {
+        @Override
+        public T validate(ServerCommandSource source, ParsedRule currentRule, T newValue, String string)
+        {
+            return (newValue.doubleValue() >= 0 && newValue.doubleValue() <= 1 )? newValue : null;
+        }
+        @Override
+        public String description() { return "Must be between 0 and 1";}
+    }
 }
diff --git a/src/main/java/carpet/utils/BlockInfo.java b/src/main/java/carpet/utils/BlockInfo.java
index 30da08fd..470ba512 100644
--- a/src/main/java/carpet/utils/BlockInfo.java
+++ b/src/main/java/carpet/utils/BlockInfo.java
@@ -109,21 +109,21 @@ public class BlockInfo
         put(Material.CARPET         , "carpet"       );
         put(Material.PLANT          , "plant"        );
         put(Material.UNDERWATER_PLANT, "water_plant" );
-        put(Material.REPLACEABLE_PLANT, "vine"       );
-        put(Material.SEAGRASS       , "sea_grass"    );
+        put(Material.REPLACEABLE_PLANT, "vegetation"       );
+        put(Material.REPLACEABLE_UNDERWATER_PLANT       , "sea_grass"    );
         put(Material.WATER          , "water"        );
         put(Material.BUBBLE_COLUMN  , "bubble_column");
         put(Material.LAVA           , "lava"         );
-        put(Material.SNOW           , "snow_layer"   );
+        put(Material.SNOW_LAYER           , "snow_layer"   );
         put(Material.FIRE           , "fire"         );
-        put(Material.PART           , "redstone_bits");
+        put(Material.SUPPORTED           , "decoration"   );
         put(Material.COBWEB         , "cobweb"       );
         put(Material.REDSTONE_LAMP  , "redstone_lamp");
-        put(Material.CLAY           , "clay"         );
-        put(Material.EARTH          , "dirt"         );
-        put(Material.ORGANIC        , "grass"        );
-        put(Material.PACKED_ICE     , "packed_ice"   );
-        put(Material.SAND           , "sand"         );
+        put(Material.ORGANIC_PRODUCT           , "clay"         );
+        put(Material.SOIL          , "dirt"         );
+        put(Material.SOLID_ORGANIC        , "grass"        );
+        put(Material.DENSE_ICE     , "packed_ice"   );
+        put(Material.AGGREGATE           , "sand"         );
         put(Material.SPONGE         , "sponge"       );
         put(Material.SHULKER_BOX    , "shulker"      );
         put(Material.WOOD           , "wood"         );
@@ -138,11 +138,11 @@ public class BlockInfo
         put(Material.STONE          , "stone"        );
         put(Material.METAL          , "metal"        );
         put(Material.SNOW_BLOCK     , "snow"         );
-        put(Material.ANVIL          , "anvil"        );
+        put(Material.REPAIR_STATION          , "anvil"        );
         put(Material.BARRIER        , "barrier"      );
         put(Material.PISTON         , "piston"       );
         put(Material.UNUSED_PLANT   , "coral"        );
-        put(Material.PUMPKIN        , "gourd"        );
+        put(Material.GOURD        , "gourd"        );
         put(Material.EGG            , "dragon_egg"   );
         put(Material.CAKE           , "cake"         );
     }};
diff --git a/src/main/java/carpet/utils/Messenger.java b/src/main/java/carpet/utils/Messenger.java
index 3b422262..7aa4f7b5 100644
--- a/src/main/java/carpet/utils/Messenger.java
+++ b/src/main/java/carpet/utils/Messenger.java
@@ -3,14 +3,15 @@ package carpet.utils;
 import net.minecraft.server.command.ServerCommandSource;
 import net.minecraft.entity.EntityCategory;
 import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.Vec3d;
-import net.minecraft.text.LiteralText;
-import net.minecraft.util.Formatting;
 import net.minecraft.text.BaseText;
-import net.minecraft.server.MinecraftServer;
 import net.minecraft.text.ClickEvent;
 import net.minecraft.text.HoverEvent;
+import net.minecraft.text.LiteralText;
+import net.minecraft.text.Style;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.Formatting;
+import net.minecraft.server.MinecraftServer;
 import net.minecraft.world.dimension.DimensionType;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -52,32 +53,33 @@ public class Messenger
      / = action added to the previous component
      */
 
-    private static BaseText _applyStyleToTextComponent(BaseText comp, String style)
+    public static Style parseStyle(String style)
     {
         //could be rewritten to be more efficient
-        comp.getStyle().setItalic(style.indexOf('i')>=0);
-        comp.getStyle().setStrikethrough(style.indexOf('s')>=0);
-        comp.getStyle().setUnderline(style.indexOf('u')>=0);
-        comp.getStyle().setBold(style.indexOf('b')>=0);
-        comp.getStyle().setObfuscated(style.indexOf('o')>=0);
-        comp.getStyle().setColor(Formatting.WHITE);
-        if (style.indexOf('w')>=0) comp.getStyle().setColor(Formatting.WHITE); // not needed
-        if (style.indexOf('y')>=0) comp.getStyle().setColor(Formatting.YELLOW);
-        if (style.indexOf('m')>=0) comp.getStyle().setColor(Formatting.LIGHT_PURPLE);
-        if (style.indexOf('r')>=0) comp.getStyle().setColor(Formatting.RED);
-        if (style.indexOf('c')>=0) comp.getStyle().setColor(Formatting.AQUA);
-        if (style.indexOf('l')>=0) comp.getStyle().setColor(Formatting.GREEN);
-        if (style.indexOf('t')>=0) comp.getStyle().setColor(Formatting.BLUE);
-        if (style.indexOf('f')>=0) comp.getStyle().setColor(Formatting.DARK_GRAY);
-        if (style.indexOf('g')>=0) comp.getStyle().setColor(Formatting.GRAY);
-        if (style.indexOf('d')>=0) comp.getStyle().setColor(Formatting.GOLD);
-        if (style.indexOf('p')>=0) comp.getStyle().setColor(Formatting.DARK_PURPLE);
-        if (style.indexOf('n')>=0) comp.getStyle().setColor(Formatting.DARK_RED);
-        if (style.indexOf('q')>=0) comp.getStyle().setColor(Formatting.DARK_AQUA);
-        if (style.indexOf('e')>=0) comp.getStyle().setColor(Formatting.DARK_GREEN);
-        if (style.indexOf('v')>=0) comp.getStyle().setColor(Formatting.DARK_BLUE);
-        if (style.indexOf('k')>=0) comp.getStyle().setColor(Formatting.BLACK);
-        return comp;
+        Style styleInstance = new Style();
+        styleInstance.setItalic(style.indexOf('i')>=0);
+        styleInstance.setStrikethrough(style.indexOf('s')>=0);
+        styleInstance.setUnderline(style.indexOf('u')>=0);
+        styleInstance.setBold(style.indexOf('b')>=0);
+        styleInstance.setObfuscated(style.indexOf('o')>=0);
+        styleInstance.setColor(Formatting.WHITE);
+        if (style.indexOf('w')>=0) styleInstance.setColor(Formatting.WHITE); // not needed
+        if (style.indexOf('y')>=0) styleInstance.setColor(Formatting.YELLOW);
+        if (style.indexOf('m')>=0) styleInstance.setColor(Formatting.LIGHT_PURPLE);
+        if (style.indexOf('r')>=0) styleInstance.setColor(Formatting.RED);
+        if (style.indexOf('c')>=0) styleInstance.setColor(Formatting.AQUA);
+        if (style.indexOf('l')>=0) styleInstance.setColor(Formatting.GREEN);
+        if (style.indexOf('t')>=0) styleInstance.setColor(Formatting.BLUE);
+        if (style.indexOf('f')>=0) styleInstance.setColor(Formatting.DARK_GRAY);
+        if (style.indexOf('g')>=0) styleInstance.setColor(Formatting.GRAY);
+        if (style.indexOf('d')>=0) styleInstance.setColor(Formatting.GOLD);
+        if (style.indexOf('p')>=0) styleInstance.setColor(Formatting.DARK_PURPLE);
+        if (style.indexOf('n')>=0) styleInstance.setColor(Formatting.DARK_RED);
+        if (style.indexOf('q')>=0) styleInstance.setColor(Formatting.DARK_AQUA);
+        if (style.indexOf('e')>=0) styleInstance.setColor(Formatting.DARK_GREEN);
+        if (style.indexOf('v')>=0) styleInstance.setColor(Formatting.DARK_BLUE);
+        if (style.indexOf('k')>=0) styleInstance.setColor(Formatting.BLACK);
+        return styleInstance;
     }
     public static String heatmap_color(double actual, double reference)
     {
@@ -114,10 +116,14 @@ public class Messenger
         {
             message = "w"+message;
         }
-        String[] parts = message.split("\\s", 2);
-        String desc = parts[0];
+        int limit = message.indexOf(' ');
+        String desc = message;
         String str = "";
-        if (parts.length > 1) str = parts[1];
+        if (limit >= 0)
+        {
+            desc = message.substring(0, limit);
+            str = message.substring(limit+1);
+        }
         if (desc.charAt(0) == '/') // deprecated
         {
             if (previous_message != null)
@@ -143,7 +149,8 @@ public class Messenger
             return previous_message;
         }
         BaseText txt = new LiteralText(str);
-        return _applyStyleToTextComponent(txt, desc);
+        txt.setStyle(parseStyle(desc));
+        return txt;
     }
     public static BaseText tp(String desc, Vec3d pos) { return tp(desc, pos.x, pos.y, pos.z); }
     public static BaseText tp(String desc, BlockPos pos) { return tp(desc, pos.getX(), pos.getY(), pos.getZ()); }
@@ -224,7 +231,8 @@ public class Messenger
     //message source
     public static void m(ServerCommandSource source, Object ... fields)
     {
-        source.sendFeedback(Messenger.c(fields),source.getMinecraftServer().getWorld(DimensionType.OVERWORLD) != null);
+        if (source != null)
+            source.sendFeedback(Messenger.c(fields),source.getMinecraftServer() != null && source.getMinecraftServer().getWorld(DimensionType.OVERWORLD) != null);
     }
     public static void m(PlayerEntity player, Object ... fields)
     {
@@ -263,7 +271,7 @@ public class Messenger
     public static BaseText s(String text, String style)
     {
         BaseText message = new LiteralText(text);
-        _applyStyleToTextComponent(message, style);
+        message.setStyle(parseStyle(style));
         return message;
     }
 
diff --git a/src/main/java/carpet/utils/MobAI.java b/src/main/java/carpet/utils/MobAI.java
index 0a202b16..dc4b3e03 100644
--- a/src/main/java/carpet/utils/MobAI.java
+++ b/src/main/java/carpet/utils/MobAI.java
@@ -4,7 +4,13 @@ import carpet.CarpetServer;
 import com.google.common.collect.Sets;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityType;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.effect.StatusEffects;
 import net.minecraft.server.world.ServerWorld;
+import net.minecraft.tag.FluidTags;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
 import net.minecraft.util.registry.Registry;
 
 import java.util.HashMap;
@@ -93,4 +99,25 @@ public class MobAI
         }
     }
 
+    /**
+     * Not a replacement for living entity jump() - this barely is to allow other entities that can't jump in vanilla to 'jump'
+     * @param e
+     */
+    public static void genericJump(Entity e)
+    {
+        if (!e.onGround && !e.isInFluid(FluidTags.WATER) && !e.isInLava()) return;
+        float m = e.world.getBlockState(new BlockPos(e)).getBlock().getJumpVelocityMultiplier();
+        float g = e.world.getBlockState(new BlockPos(e.getX(), e.getBoundingBox().y1 - 0.5000001D, e.getZ())).getBlock().getJumpVelocityMultiplier();
+        float jumpVelocityMultiplier = (double) m == 1.0D ? g : m;
+        float jumpStrength = (0.42F * jumpVelocityMultiplier);
+        Vec3d vec3d = e.getVelocity();
+        e.setVelocity(vec3d.x, jumpStrength, vec3d.z);
+        if (e.isSprinting())
+        {
+            float u = e.yaw * 0.017453292F;
+            e.setVelocity(e.getVelocity().add((-MathHelper.sin(g) * 0.2F), 0.0D, (MathHelper.cos(u) * 0.2F)));
+        }
+        e.velocityDirty = true;
+    }
+
 }
diff --git a/src/main/java/carpet/utils/SpawnChunks.java b/src/main/java/carpet/utils/SpawnChunks.java
new file mode 100644
index 00000000..f2e8f117
--- /dev/null
+++ b/src/main/java/carpet/utils/SpawnChunks.java
@@ -0,0 +1,16 @@
+package carpet.utils;
+
+import carpet.fakes.ChunkTicketManagerInterface;
+import carpet.fakes.ServerChunkManagerInterface;
+import net.minecraft.server.world.ChunkTicketManager;
+import net.minecraft.server.world.ServerChunkManager;
+import net.minecraft.util.math.ChunkPos;
+
+public class SpawnChunks
+{
+    public static void changeSpawnChunks(ServerChunkManager chunkManager, ChunkPos pos, int size)
+    {
+        ChunkTicketManager ticketManager = ((ServerChunkManagerInterface)chunkManager).getCMTicketManager();
+        ((ChunkTicketManagerInterface)ticketManager).changeSpawnChunks(pos, size);
+    }
+}
diff --git a/src/main/java/carpet/utils/SpawnReporter.java b/src/main/java/carpet/utils/SpawnReporter.java
index defd6071..4e6cd8a5 100644
--- a/src/main/java/carpet/utils/SpawnReporter.java
+++ b/src/main/java/carpet/utils/SpawnReporter.java
@@ -186,7 +186,7 @@ public class SpawnReporter
             }
             else
             {
-                return printEntitiesByType(creature_type, worldIn);
+                return printEntitiesByType(creature_type, worldIn, true);
                 
             }
             
@@ -250,22 +250,24 @@ public class SpawnReporter
         return get_type_string(get_creature_type_from_code(str));
     }
     
-    public static List printEntitiesByType(EntityCategory cat, World worldIn) //Class entityType)
+    public static List printEntitiesByType(EntityCategory cat, World worldIn, boolean all) //Class entityType)
     {
         List lst = new ArrayList<>();
         lst.add( Messenger.s(String.format("Loaded entities for %s class:", get_type_string(cat))));
         for (Entity entity : ((ServerWorld)worldIn).getEntities(null, (e) -> e.getType().getCategory()==cat))
         {
-            if (!(entity instanceof MobEntity) || !((MobEntity)entity).isPersistent())
-            {
-                EntityType type = entity.getType();
-                BlockPos pos = entity.getBlockPos();
-                lst.add( Messenger.c(
-                        "w  - ",
-                        Messenger.tp("wb",pos),
-                        String.format("w : %s", type.getName().getString())
-                        ));
-            }
+            boolean persistent = entity instanceof MobEntity && ( ((MobEntity) entity).isPersistent() || ((MobEntity) entity).cannotDespawn());
+            if (!all && persistent)
+                continue;
+
+            EntityType type = entity.getType();
+            BlockPos pos = entity.getBlockPos();
+            lst.add( Messenger.c(
+                    "w  - ",
+                    Messenger.tp(persistent?"gb":"wb",pos),
+                    String.format(persistent?"g : %s":"w : %s", type.getName().getString())
+            ));
+
         }
         if (lst.size()==1)
         {
diff --git a/src/main/java/carpet/utils/Translations.java b/src/main/java/carpet/utils/Translations.java
new file mode 100644
index 00000000..b27be4b1
--- /dev/null
+++ b/src/main/java/carpet/utils/Translations.java
@@ -0,0 +1,94 @@
+package carpet.utils;
+
+import carpet.CarpetExtension;
+import carpet.CarpetServer;
+import carpet.CarpetSettings;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import net.minecraft.server.command.ServerCommandSource;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+public class Translations
+{
+    private static Map translationMap;
+
+    public static String tr(String key)
+    {
+        return translationMap == null ? key : translationMap.getOrDefault(key, key);
+    }
+
+    public static String tr(String key, String str)
+    {
+        return translationMap == null ? str : translationMap.getOrDefault(key, str);
+    }
+
+    public static boolean hasTranslations()
+    {
+        return translationMap != null;
+    }
+
+    public static boolean hasTranslation(String key)
+    {
+        return translationMap != null && translationMap.containsKey(key);
+    }
+
+    public static Map getTranslationFromResourcePath(String path)
+    {
+        String dataJSON;
+        try
+        {
+            dataJSON = IOUtils.toString(
+                    Objects.requireNonNull(Translations.class.getClassLoader().getResourceAsStream(String.format("assets/carpet/lang/%s.json", CarpetSettings.language))),
+                    StandardCharsets.UTF_8);
+        } catch (NullPointerException | IOException e) {
+            return null;
+        }
+        Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
+        return gson.fromJson(dataJSON, new TypeToken>() {}.getType());
+    }
+
+
+    public static void updateLanguage(ServerCommandSource source)
+    {
+        if (CarpetSettings.language.equalsIgnoreCase("none"))
+        {
+            translationMap = null;
+            return;
+        }
+        Map translations = new HashMap<>();
+        Map trans = getTranslationFromResourcePath(String.format("assets/carpet/lang/%s.json", CarpetSettings.language));
+        if (trans != null) trans.forEach(translations::put);
+
+        for (CarpetExtension ext : CarpetServer.extensions)
+        {
+            Map extMappings = ext.canHasTranslations(CarpetSettings.language);
+            if (extMappings != null)
+            {
+                extMappings.forEach((key, value) ->
+                {
+                    if (!translations.containsKey(key)) translations.put(key, value);
+                });
+            }
+        }
+        translations.entrySet().removeIf(e -> e.getKey().startsWith("//"));
+        if (translations.isEmpty())
+        {
+            translationMap = null;
+            return;
+        }
+        translationMap = translations;
+    }
+
+    public static boolean isValidLanguage(String newValue)
+    {
+        // will put some validations for availble languages at some point
+        return true;
+    }
+}
diff --git a/src/main/resources/assets/carpet/lang/en_us.json b/src/main/resources/assets/carpet/lang/en_us.json
new file mode 100644
index 00000000..34331d5c
--- /dev/null
+++ b/src/main/resources/assets/carpet/lang/en_us.json
@@ -0,0 +1,53 @@
+// This file isn't necessary at all, because the language module would use the strings coded in the source if language "none" is selected.
+// So en_us is the default language and probably no change will be made after loading this file.
+// But it could be a good example for writing the language file for other languages.
+
+{
+  "// Rules:": "",
+  "rule.portalSuffocationFix.name": "",
+  "// the key is genetated as:" : "String.format('rule.%s.name', rule.name)",
+
+
+  "rule.portalSuffocationFix.desc": "",
+  "// the key is genetated as:" : "String.format('rule.%s.desc', rule.name)",
+
+
+  "rule.portalSuffocationFix.extra.0": "",
+  "rule.portalSuffocationFix.extra.1": "",
+  "// the key is genetated as:" : "String.format('rule.%s.extra.%i', rule.name, index)",
+
+
+  "// Categories:": "",
+
+  "category.bugfix": "bugfix",
+  "category.survival": "survival",
+  "category.creative": "creative",
+  "category.experimental": "experimental",
+  "category.optimization": "optimization",
+  "category.feature": "feature",
+  "category.command": "command",
+  "category.tnt": "TNT",
+  "category.dispenser": "dispenser",
+  "category.scarpet": "scarpet",
+  "category.client": "client",
+
+  "// Settings command UI elements": "",
+
+  "ui.browse_categories": "Browse categories",
+  "ui.list_all_%(cat)s_settings": "list all %s settings",
+  "ui.version": "Version",
+  "ui.current_%(mod)s_settings": "Current %s settings",
+  "ui.switch_to": "Switch to",
+  "ui.settings_are_locked": "settings are locked",
+  "ui.is_set_to": "is set to",
+  "ui.unknown_rule": "Unknown rule",
+  "ui.current_%(mod)s_startup_settings_from_%(conf)s": "Current %s Startup Settings from %s",
+  "ui.%(mod)s_settings_matching_'%(query)s'": "%s settings matching \"%s\" ",
+  "ui.all_%(mod)s_settings": "All %s settings",
+  "ui.tags": "Tags",
+  "ui.change_permanently": "Change permanently",
+  "ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts": "click to keep the settings in %s to save across restarts",
+  "ui.rule_%(rule)s_will_now_default_to_%(value)s": "rule %s will now default to %s",
+  "ui.current_value": "current value"
+
+}
diff --git a/src/main/resources/assets/carpet/lang/zh_cn.json b/src/main/resources/assets/carpet/lang/zh_cn.json
new file mode 100644
index 00000000..ef0c0124
--- /dev/null
+++ b/src/main/resources/assets/carpet/lang/zh_cn.json
@@ -0,0 +1,234 @@
+{
+  "rule.antiCheatDisabled.name": "移动检测通杀",
+  "rule.antiCheatDisabled.desc": "防止玩家因为“本服务器未启用飞行”,“移动过快”等原因被踢出",
+  
+  "rule.carpets.name": "放置地毯执行功能",
+  "rule.carpets.desc": "通过放置地毯方块快捷执行部分功能",
+  
+  "rule.combineXPOrbs.name": "经验球合并",
+  "rule.combineXPOrbs.desc": "会将多个小经验球合并为更大的经验球",
+  
+  "rule.commandCameramode.name": "灵魂出窍模式",
+  "rule.commandCameramode.desc": "启用/c和/s命令使得玩家可以在生存模式和观察者模式间切换",
+  "rule.commandCameramode.extra.0": "/c和/s命令所有玩家(包括非OP玩家)都可以使用",
+  
+  "rule.commandDistance.name": "距离测量",
+  "rule.commandDistance.desc": "启用/distance命令以测量游戏中两点间距离",
+  "rule.commandDistance.extra.0": "可以通过放置棕色地毯的方式使用本功能,前提是“放置地毯执行功能”已启用",
+  
+  "rule.commandDraw.name": "画图",
+  "rule.commandDraw.desc": "启用/draw命令以允许绘制一些简单图形",
+  
+  "rule.commandInfo.name": "获取方块数据",
+  "rule.commandInfo.desc": "启用/info命令以获取方块数据",
+  "rule.commandInfo.extra.0": "可以通过放置灰色地毯的方式使用本功能,前提是“放置地毯执行功能”已启用",
+  
+  "rule.commandLog.name": "游戏数据监视器",
+  "rule.commandLog.desc": "启用/log命令以在聊天栏/Tab栏监视游戏部分数据",
+  
+  "rule.commandPerimeterInfo.name": "刷怪区域搜寻器",
+  "rule.commandPerimeterInfo.desc": "启用/perimeterinfo命令以搜寻周围的可刷怪区域",
+  
+  "rule.commandPlayer.name": "假人控制",
+  "rule.commandPlayer.desc": "启用/player命令以控制/召唤玩家",
+  
+  "rule.commandScript.name": "地毯脚本控制器",
+  "rule.commandScript.desc": "启用/script相关命令",
+  "rule.commandScript.extra.0": "一个Scarpet语言的游戏内API",
+  
+  "rule.commandSpawn.name": "刷怪追踪器",
+  "rule.commandSpawn.desc": "启用/spawn命令用于怪物生成追踪",
+  
+  "rule.commandTick.name": "游戏时钟控制器",
+  "rule.commandTick.desc": "启用/tick命令以控制游戏时钟",
+  
+  "rule.commandTrackAI.name": "AI追踪器",
+  "rule.commandTrackAI.desc": "启用/track命令以追踪生物的AI",
+  
+  "rule.ctrlQCraftingFix.name": "ctrlQ合成修复",
+  "rule.ctrlQCraftingFix.desc": "在合成时允许使用ctrl+Q快捷键扔出结果格内所有物品",
+  
+  "rule.customMOTD.name": "MOTD更改",
+  "rule.customMOTD.desc": "设置一个不同的MOTD信息",
+  "rule.customMOTD.extra.0": "使用'_'时将发送默认MOTD(从server.properties中读取)",
+  
+  "rule.defaultLoggers.name": "默认游戏监视器",
+  "rule.defaultLoggers.desc": "为所有新进入玩家设置部分监视器默认开启",
+  "rule.defaultLoggers.extra.0": "比如设置'tps,mobcaps'作为默认监视器",
+  
+  "rule.desertShrubs.name": "树苗在沙漠干枯",
+  "rule.desertShrubs.desc": "树苗将在附近无水源的沙漠群系枯萎",
+  
+  "rule.disableSpawnChunks.name": "允许出生点区块卸载",
+  "rule.disableSpawnChunks.desc": "允许出生点区块卸载",
+  
+  "rule.explosionNoBlockDamage.name": "防爆",
+  "rule.explosionNoBlockDamage.desc": "任何形式的爆炸都不会摧毁方块",
+  
+  "rule.extremeBehaviours.name": "随机极端情况",
+  "rule.extremeBehaviours.desc": "大幅度提升极端情况出现概率,仅用于测试",
+  "rule.extremeBehaviours.extra.0": "发射器和投掷器将使用最大随机刻",
+  
+  "rule.fastRedstoneDust.name": "红石粉卡顿优化",
+  "rule.fastRedstoneDust.desc": "红石粉卡顿优化 作者 Theosib",
+  
+  "rule.fillLimit.name": "fill/clone上限更改",
+  "rule.fillLimit.desc": "将fill/clone上限更改为自己需要的值",
+  "rule.fillLimit.extra.0": "你必须在1-20M之间取一个值",
+  
+  "rule.fillUpdates.name": "fill更新开关",
+  "rule.fillUpdates.desc": "fill/clone/setblock命令以及结构方块执行时是否产生更新",
+  "rule.fillUpdates.extra.0": "设置为true有更新 设置为false时为无更新",
+  
+  "rule.flatWorldStructureSpawning.name": "结构怪在超平坦世界生成",
+  "rule.flatWorldStructureSpawning.desc": "允许结构怪在超平坦世界生成",
+  
+  "rule.flippinCactus.name": "仙人掌扳手",
+  "rule.flippinCactus.desc": "允许使用仙人掌调整方块朝向,并且不会产生更新",
+  
+  "rule.hardcodeTNTangle.name": "硬编码TNT角度",
+  "rule.hardcodeTNTangle.desc": "把TNT水平随机角度设为固定值,可用于测试机器",
+  "rule.hardcodeTNTangle.extra.0": "默认为-1,必须在0与360之间,或者-1",
+
+  "rule.hopperCounters.name": "漏斗计数器",
+  "rule.hopperCounters.desc": "漏斗指向的羊毛会计算经过的物品",
+  "rule.hopperCounters.extra.0": "在羊毛上放置红色或绿色地毯,使用/counter命令可启用",
+  "rule.hopperCounters.extra.1": "使用 /counter  reset重置计数器,使用/counter 查询",
+  "rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放绿地毯可以查询,放红地毯重置",
+  "rule.hopperCounters.extra.3": "数据是全局性在玩家间共享的,并且有16颜色可用",
+  "rule.hopperCounters.extra.4": "物品会被销毁,每个漏斗每tick销毁一组物品",
+
+  "rule.huskSpawningInTemples.name": "尸壳在神殿里生成",
+  "rule.huskSpawningInTemples.desc": "只有尸壳会在沙漠神殿生成",
+
+  "rule.lagFreeSpawning.name": "延迟自然生成",
+  "rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和内存",
+
+  "rule.leadFix.name": "栓绳修复",
+  "rule.leadFix.desc": "修复栓绳在未加载区块中,变得不可见的问题",
+
+  "rule.maxEntityCollisions.name": "最大实体挤压数",
+  "rule.maxEntityCollisions.desc": "设定最大实体挤压限制,0表示无限大",
+
+  "rule.mergeTNT.name": "合并TNT",
+  "rule.mergeTNT.desc": "合并静止的点燃的tnt实体",
+
+  "rule.missingTools.name": "工具缺失修复",
+  "rule.missingTools.desc": "活塞,玻璃和海绵可以用合适的工具更快地破坏",
+
+  "rule.movableBlockEntities.name": "容器移动",
+  "rule.movableBlockEntities.desc": "活塞可推动方块实体,比如箱子,漏斗",
+
+  "rule.onePlayerSleeping.name": "单个玩家睡觉",
+  "rule.onePlayerSleeping.desc": "服务器里一个玩家睡觉可跳过夜晚",
+
+  "rule.optimizedTNT.name": "TNT优化",
+  "rule.optimizedTNT.desc": "tnt在相同的地点或在流体里,爆炸造成更小的延迟",
+
+  "rule.persistentParrots.name": "鹦鹉停留",
+  "rule.persistentParrots.desc": "鹦鹉不会离开你的肩膀,除非你收到伤害",
+
+  "rule.placementRotationFix.name": "放置旋转修复",
+  "rule.placementRotationFix.desc": "修复了玩家放置方块时快速转身造成的问题",
+  
+  "rule.pushLimit.name": "自定义推动上限",
+  "rule.pushLimit.desc": "自定义活塞推动上限",
+  "rule.pushLimit.extra.0": "你必须在1-1024中选取一个值",
+  
+  "rule.quasiConnectivity.name": "qC激活开关",
+  "rule.quasiConnectivity.desc": "活塞,发射器和投掷器在他们上方方块激活时是否响应",
+  "rule.quasiConnectivity.extra.0": "设置为true为开启 设置为false为禁用",
+  
+  "rule.railPowerLimit.name": "自定义动力铁轨激活距离",
+  "rule.railPowerLimit.desc": "动力铁轨激活上限可自定义",
+  "rule.railPowerLimit.extra.0": "你必须在1-1024中选取一个值",
+  
+  "rule.renewableCoral.name": "珊瑚可再生",
+  "rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以生成珊瑚结构",
+  
+  "rule.renewableSponges.name": "海绵可再生",
+  "rule.renewableSponges.desc": "守卫者将在被雷击后会变为远古守卫者",
+  
+  "rule.rotatorBlock.name": "仙人掌扳手发射器版",
+  "rule.rotatorBlock.desc": "发射器中的仙人掌可以旋转方块(尽可能以逆时针)",
+  
+  "rule.scriptsAutoload.name": "Scarpet脚本自动加载",
+  "rule.scriptsAutoload.desc": "在服务器/世界加载的时候将会自动从世界文件中加载Scarpet脚本文件",
+  "rule.scriptsAutoload.extra.0": "/script命令必须启用",
+  
+  "rule.shulkerSpawningInEndCities.name": "潜影贝重生",
+  "rule.shulkerSpawningInEndCities.desc": "潜影贝将会在末地城中重生",
+  
+  "rule.silverFishDropGravel.name": "蠹虫钻出方块掉落沙砾",
+  "rule.silverFishDropGravel.desc": "蠹虫钻出方块时会掉落沙砾",
+  
+  "rule.smoothClientAnimations.name": "平滑客户端动画",
+  "rule.smoothClientAnimations.desc": "低TPS设置时可以使客户端动画更加平滑",
+  "rule.smoothClientAnimations.extra.0": "仅在单人游戏中生效,并且会降低客户端运行速度",
+  
+  "rule.stackableShulkerBoxes.name": "潜影盒堆叠",
+  "rule.stackableShulkerBoxes.desc": "空潜影盒仍在地上时将自动堆叠,直至堆叠为1组",
+  "rule.stackableShulkerBoxes.extra.0": "在物品GUI中,请使用shift键+单击以移动整组潜影盒",
+  
+  "rule.summonNaturalLightning.name": "召唤闪电自然化",
+  "rule.summonNaturalLightning.desc": "召唤的闪电将拥有所有自然生成闪电拥有的特性",
+  
+  "rule.tntDoNotUpdate.name": "TNT放置时不产生更新",
+  "rule.tntDoNotUpdate.desc": "TNT在放置时不会产生任何方块更新(不会激活)",
+  
+  "rule.tntPrimerMomentumRemoved.name": "移除TNT点燃时随机动量",
+  "rule.tntPrimerMomentumRemoved.desc": "TNT点燃时的随机动量将被移除",
+  
+  "rule.tntRandomRange.name": "TNT爆炸范围设置",
+  "rule.tntRandomRange.desc": "设置TNT随机爆炸范围为一个固定的值,设为-1以禁用",
+  "rule.tntRandomRange.extra.0": "必须启用optimizedTNT功能",
+  "rule.tntRandomRange.extra.1": "不能为负值,除了-1",
+  
+  "rule.unloadedEntityFix.name": "未加载实体修复",
+  "rule.unloadedEntityFix.desc": "进入或者被推入未加载区块的实体不会消失",
+  
+  "rule.viewDistance.name": "自定义视距",
+  "rule.viewDistance.desc": "改变服务器视距,设为0以使用默认值",
+  "rule.viewDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数",
+  
+  "rule.xpNoCooldown.name": "经验吸收无冷却",
+  "rule.xpNoCooldown.desc": "玩家吸收经验球时不会有冷却时间",
+  
+  "rule.portalSuffocationFix.name": "传送门窒息修复",
+  "rule.portalSuffocationFix.desc": "使下界传送门正确地传送实体",
+  "rule.portalSuffocationFix.extra.0": "传送的实体将不再卡在黑曜石里面",
+  
+  "rule.portalCreativeDelay.name": "创造玩家不被地狱门传送",
+  "rule.portalCreativeDelay.desc": "创造模式的玩家将不会立即被地狱门传送",
+
+
+  "category.bugfix": "BUG修复",
+  "category.survival": "生存",
+  "category.creative": "创造",
+  "category.experimental": "试验性",
+  "category.optimization": "优化",
+  "category.feature": "特性",
+  "category.command": "指令",
+  "category.tnt": "TNT",
+  "category.dispenser": "发射器",
+  "category.scarpet": "scarpet脚本语言",
+  "category.client": "客户端",
+
+  "ui.browse_categories": "浏览分类",
+  "ui.list_all_%(cat)s_settings": "列出所有 %s 选项",
+  "ui.version": "版本",
+  "ui.current_%(mod)s_settings": "目前启用的 %s 选项",
+  "ui.switch_to": "切换至",
+  "ui.settings_are_locked": "选项被锁定",
+  "ui.is_set_to": "被设置为",
+  "ui.unknown_rule": "未知的规则选项",
+  "ui.current_%(mod)s_startup_settings_from_%(conf)s": "目前启用的%s选项,从%s中读取得",
+  "ui.%(mod)s_settings_matching_'%(query)s'": "%s中符合\"%s\"的选项:",
+  "ui.all_%(mod)s_settings": "所有的%s选项",
+  "ui.tags": "分类",
+  "ui.change_permanently": "永久更改",
+  "ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts": "点击此按钮以使此选项保存于%s,重启后依旧生效",
+  "ui.rule_%(rule)s_will_now_default_to_%(value)s": "规则%s将会被默认设置为%s",
+  "ui.current_value": "当前的值"
+
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/carpet/lang/zh_tw.json b/src/main/resources/assets/carpet/lang/zh_tw.json
new file mode 100644
index 00000000..a3ede4a1
--- /dev/null
+++ b/src/main/resources/assets/carpet/lang/zh_tw.json
@@ -0,0 +1,234 @@
+{
+  "rule.antiCheatDisabled.name": "關閉伺服器作弊檢測",
+  "rule.antiCheatDisabled.desc": "防止玩家因為“本伺服器不允許飛行”,“移動速度過快”等原因被請出伺服器",
+
+  "rule.carpets.name": "放置地毯顯示進階數據",
+  "rule.carpets.desc": "通過把地毯放置在地上取得更進階信息",
+
+  "rule.combineXPOrbs.name": "合併小經驗球",
+  "rule.combineXPOrbs.desc": "會將大量的小經驗球合併成等效的大經驗球",
+
+  "rule.commandCameramode.name": "旁觀者模式",
+  "rule.commandCameramode.desc": "允許玩家使用/c切換成旁觀者模式,利用/s切換回生存模式",
+  "rule.commandCameramode.extra.0": "/c及/s不需要op權限也可使用",
+
+  "rule.commandDistance.name": "距離測定",
+  "rule.commandDistance.desc": "使用/distance指令測定兩點的距離",
+  "rule.commandDistance.extra.0": "可以使用放置棕色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用",
+
+  "rule.commandDraw.name": "繪製圖形",
+  "rule.commandDraw.desc": "使用/draw指令以允許繪製一些簡易圖形",
+
+  "rule.commandInfo.name": "獲取方塊進階資訊",
+  "rule.commandInfo.desc": "允許使用/info指令以獲得方塊進階資訊",
+  "rule.commandInfo.extra.0": "可以使用放置灰色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用",
+
+  "rule.commandLog.name": "遊戲數據追蹤器",
+  "rule.commandLog.desc": "允許使用/log指令使在Tab欄位追蹤遊戲部分數據",
+
+  "rule.commandPerimeterInfo.name": "刷怪區域追蹤器",
+  "rule.commandPerimeterInfo.desc": "允許使用/perimeterinfo指令以探查周圍可生怪區域",
+
+  "rule.commandPlayer.name": "機器人控制",
+  "rule.commandPlayer.desc": "允許使用/player指令以控制/召喚已存在的玩家id作為機器人",
+
+  "rule.commandScript.name": "地毯腳本控制器",
+  "rule.commandScript.desc": "允許使用/script相關指令",
+  "rule.commandScript.extra.0": "一個Scarpet程式語言的遊戲內API",
+
+  "rule.commandSpawn.name": "刷怪追蹤器",
+  "rule.commandSpawn.desc": "允許使用/spawn指令用於怪物生成追蹤",
+
+  "rule.commandTick.name": "遊戲時鐘控制器",
+  "rule.commandTick.desc": "允許使用/tick指令控制遊戲內的伺服器端運行速度",
+
+  "rule.commandTrackAI.name": "AI追蹤器",
+  "rule.commandTrackAI.desc": "允許使用/track指令以追蹤生物的AI",
+
+  "rule.ctrlQCraftingFix.name": "ctrlQ合成修復",
+  "rule.ctrlQCraftingFix.desc": "在合成時允許使用ctrl+Q快捷鍵扔出結果格內所有物品",
+
+  "rule.customMOTD.name": "MOTD更改",
+  "rule.customMOTD.desc": "設置一個不同的MOTD信息",
+  "rule.customMOTD.extra.0": "設置成'_'時將採用默認的MOTD(從server.properties中讀取的)",
+
+  "rule.defaultLoggers.name": "默認開啟的遊戲數據追蹤器",
+  "rule.defaultLoggers.desc": "為所有新加入的玩家設置部分遊戲數據追蹤器默認為開啟狀態",
+  "rule.defaultLoggers.extra.0": "比如設置'tps,mobcaps'最為默認遊戲數據追蹤",
+
+  "rule.desertShrubs.name": "樹苗在沙漠中乾枯",
+  "rule.desertShrubs.desc": "樹苗將在附近無水源狀態的沙漠生態群系變成枯灌木",
+
+  "rule.disableSpawnChunks.name": "允許出生點區塊被卸載",
+  "rule.disableSpawnChunks.desc": "允許出生點區塊被卸載",
+
+  "rule.explosionNoBlockDamage.name": "爆炸不破壞方塊",
+  "rule.explosionNoBlockDamage.desc": "任何形式的爆炸都無法摧毀方塊",
+
+  "rule.extremeBehaviours.name": "隨機參數極端化",
+  "rule.extremeBehaviours.desc": "大幅度提升極端情況出現的概率,僅用於測試",
+  "rule.extremeBehaviours.extra.0": "發射器和投擲器將使用最大隨機刻",
+
+  "rule.fastRedstoneDust.name": "紅石粉卡頓優化",
+  "rule.fastRedstoneDust.desc": "紅石粉卡頓優化 作者 Theosib",
+
+  "rule.fillLimit.name": "fill/clone上限更改",
+  "rule.fillLimit.desc": "將fill/clone上限更改為自己設定的值",
+  "rule.fillLimit.extra.0": "你必須在1-2千萬之間取一個整數值",
+
+  "rule.fillUpdates.name": "fill更新設置",
+  "rule.fillUpdates.desc": "fill/clone/setblock指令以及結構方塊執行時是否產生更新",
+  "rule.fillUpdates.extra.0": "設置為true時有更新 設置為false時為無更新",
+
+  "rule.flatWorldStructureSpawning.name": "特定建築怪在超平坦世界生成",
+  "rule.flatWorldStructureSpawning.desc": "允許特定建築的怪物在超平坦世界生成",
+
+  "rule.flippinCactus.name": "仙人掌扳手",
+  "rule.flippinCactus.desc": "允許使用仙人掌調整方塊朝向,並且不會造成更新",
+
+  "rule.hardcodeTNTangle.name": "強制TNT角度",
+  "rule.hardcodeTNTangle.desc": "把TNT水平隨機角度設定為固定值,可用於測試機器",
+  "rule.hardcodeTNTangle.extra.0": "默認為-1,必須在0與360之間,或者-1",
+
+  "rule.hopperCounters.name": "漏斗計數器",
+  "rule.hopperCounters.desc": "漏斗指向的羊毛方塊會計算吸收的物品並清除",
+  "rule.hopperCounters.extra.0": "在羊毛方塊上放置紅色或綠色地毯,使用/counter指令可啟用",
+  "rule.hopperCounters.extra.1": "使用 /counter  reset重置計數器,使用/counter 查詢資訊",
+  "rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放置綠色地毯可以查詢,放紅色地毯重置",
+  "rule.hopperCounters.extra.3": "數據是全伺服器玩家共享的,並且有16色可以使用",
+  "rule.hopperCounters.extra.4": "物品會被銷毀,每個漏斗每tick銷毀一組物品",
+
+  "rule.huskSpawningInTemples.name": "屍殼在沙漠神殿裡生成",
+  "rule.huskSpawningInTemples.desc": "只有屍殼會在沙漠神殿生成",
+
+  "rule.lagFreeSpawning.name": "延遲自然生成",
+  "rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和RAM",
+
+  "rule.leadFix.name": "栓繩修復",
+  "rule.leadFix.desc": "修復栓繩在未加載區塊中,變得不可見的問題",
+
+  "rule.maxEntityCollisions.name": "最大實體擠壓數量",
+  "rule.maxEntityCollisions.desc": "設定最大實體擠壓限制,0表示無限大",
+
+  "rule.mergeTNT.name": "合併TNT",
+  "rule.mergeTNT.desc": "合併靜止的點燃的tnt實體",
+
+  "rule.missingTools.name": "工具缺失修復",
+  "rule.missingTools.desc": "活塞,玻璃和海綿可以使用合適的工具更快地破壞",
+
+  "rule.movableBlockEntities.name": "容器方塊移動",
+  "rule.movableBlockEntities.desc": "活塞可以推動具有容器標籤的方塊,比如箱子,漏斗",
+
+  "rule.onePlayerSleeping.name": "單個玩家睡覺跳過夜晚",
+  "rule.onePlayerSleeping.desc": "只要伺服器中其中一人睡覺即可跳過全伺服器夜晚",
+
+  "rule.optimizedTNT.name": "TNT優化",
+  "rule.optimizedTNT.desc": "tnt在相同位置或者在流體裡,爆炸將造成更低延遲",
+
+  "rule.persistentParrots.name": "鸚鵡停留",
+  "rule.persistentParrots.desc": "鸚鵡不會離開你的肩膀,除非你受到傷害",
+
+  "rule.placementRotationFix.name": "放置旋轉問題修復",
+  "rule.placementRotationFix.desc": "修復玩家放置方塊時快速轉身造成的問題",
+
+  "rule.pushLimit.name": "自訂義推動上限",
+  "rule.pushLimit.desc": "自訂義活塞推動上限",
+  "rule.pushLimit.extra.0": "你必須在1-1024中選取一個值,預設是12",
+
+  "rule.quasiConnectivity.name": "半連接激活開關",
+  "rule.quasiConnectivity.desc": "活塞,發射器和投擲器在他們上方方塊激活時是否響應",
+  "rule.quasiConnectivity.extra.0": "設置為true為開啟 設置為false為禁用",
+
+  "rule.railPowerLimit.name": "自訂義動力鐵軌激活距離",
+  "rule.railPowerLimit.desc": "動力鐵軌激活距離能在此更改",
+  "rule.railPowerLimit.extra.0": "你必須在1-1024中選取一個數值",
+
+  "rule.renewableCoral.name": "可再生珊瑚",
+  "rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以長成珊瑚方塊結構",
+
+  "rule.renewableSponges.name": "可再生海綿",
+  "rule.renewableSponges.desc": "深海守衛將在被雷擊後變成遠古深海守衛",
+
+  "rule.rotatorBlock.name": "仙人掌扳手發射器版",
+  "rule.rotatorBlock.desc": "發射器中的仙人掌可以旋轉方塊(盡可能以逆時針)",
+
+  "rule.scriptsAutoload.name": "Scarpet腳本自動加載",
+  "rule.scriptsAutoload.desc": "在伺服器/世界加載的時候將會自動從世界文件中加載Scarpet腳本文件",
+  "rule.scriptsAutoload.extra.0": "/script指令必須為啟用狀態",
+
+  "rule.shulkerSpawningInEndCities.name": "可重生界伏蚌",
+  "rule.shulkerSpawningInEndCities.desc": "界伏蚌將會在終界城市中重生",
+
+  "rule.silverFishDropGravel.name": "蠹魚鑽出放塊掉落礫石",
+  "rule.silverFishDropGravel.desc": "蠹魚鑽出放塊時會掉落礫石",
+
+  "rule.smoothClientAnimations.name": "更平暢玩家端動畫",
+  "rule.smoothClientAnimations.desc": "降低TPS設置時可以使玩家端動畫更加平暢",
+  "rule.smoothClientAnimations.extra.0": "僅在單人模式中生效,並且會降低玩家端運行速度",
+
+  "rule.stackableShulkerBoxes.name": "可堆疊界伏盒",
+  "rule.stackableShulkerBoxes.desc": "空的界伏盒扔在地上時將會自動堆疊,直到堆疊為1組",
+  "rule.stackableShulkerBoxes.extra.0": "在物品GUI中,請使用shift鍵+單擊以移動整組界伏盒",
+
+  "rule.summonNaturalLightning.name": "召喚閃電自然化",
+  "rule.summonNaturalLightning.desc": "召喚的閃電將具有自然生成時的特性",
+
+  "rule.tntDoNotUpdate.name": "TNT放置時不會被更新",
+  "rule.tntDoNotUpdate.desc": "TNT在放置時不會產生任何方塊更新(不會激活)",
+
+  "rule.tntPrimerMomentumRemoved.name": "移除TNT點燃時隨機動量",
+  "rule.tntPrimerMomentumRemoved.desc": "TNT點燃時的隨機動量將被移除",
+
+  "rule.tntRandomRange.name": "TNT爆炸範圍設置",
+  "rule.tntRandomRange.desc": "設置TNT隨機爆炸範圍為一個固定的值,設為-1以禁用",
+  "rule.tntRandomRange.extra.0": "必須啟用optimizedTNT功能",
+  "rule.tntRandomRange.extra.1": "不能設置為負數,除了-1",
+
+  "rule.unloadedEntityFix.name": "未加載實體修復",
+  "rule.unloadedEntityFix.desc": "進入或者被推入未加載區塊的實體不會消失",
+
+  "rule.viewDistance.name": "自訂義視野",
+  "rule.viewDistance.desc": "改變伺服器端視野距離,設置為0以使用默認值",
+  "rule.viewDistance.extra.0": "你必須在0(使用伺服器默認設置)-32取一個整數",
+
+  "rule.xpNoCooldown.name": "經驗球吸收無冷卻",
+  "rule.xpNoCooldown.desc": "玩家吸收經驗球不會有冷卻時間",
+
+  "rule.portalSuffocationFix.name": "傳送門窒息修復",
+  "rule.portalSuffocationFix.desc": "使地獄傳送門正確地傳送實體",
+  "rule.portalSuffocationFix.extra.0": "傳送的實體將不再卡在黑曜石裡",
+
+  "rule.portalCreativeDelay.name": "創造模式玩家地獄門傳送延遲",
+  "rule.portalCreativeDelay.desc": "創造模式下使用地獄傳送門將同生存時產生延遲",
+
+
+  "category.bugfix": "BUG修復",
+  "category.survival": "生存用",
+  "category.creative": "創造用",
+  "category.experimental": "實驗用",
+  "category.optimization": "優化",
+  "category.feature": "特性",
+  "category.command": "指令",
+  "category.tnt": "TNT",
+  "category.dispenser": "發射器",
+  "category.scarpet": "scarpet腳本程式語言",
+  "category.client": "玩家端",
+
+  "ui.browse_categories": "瀏覽分類",
+  "ui.list_all_%(cat)s_settings": "列出所有 %s 選項",
+  "ui.version": "版本",
+  "ui.current_%(mod)s_settings": "目前啟用的 %s 選項",
+  "ui.switch_to": "切換至",
+  "ui.settings_are_locked": "選項被鎖定",
+  "ui.is_set_to": "被設置為",
+  "ui.unknown_rule": "未知的規則選項",
+  "ui.current_%(mod)s_startup_settings_from_%(conf)s": "目前啟用的%s選項,從%s中讀取得",
+  "ui.%(mod)s_settings_matching_'%(query)s'": "%s中符合\"%s\"的選項:",
+  "ui.all_%(mod)s_settings": "所有的%s選項",
+  "ui.tags": "分類",
+  "ui.change_permanently": "設為預設",
+  "ui.click_to_keep_the_settings_in_%(conf)s_to_save_across_restarts": "點擊此按鈕以使此選項保存於%s,重啟後依舊生效",
+  "ui.rule_%(rule)s_will_now_default_to_%(value)s": "規則%s將會被默認設置為%s",
+  "ui.current_value": "當前的值"
+
+}
diff --git a/src/main/resources/assets/carpet/scripts/camera.sc b/src/main/resources/assets/carpet/scripts/camera.sc
index 57bf59f2..993825db 100644
--- a/src/main/resources/assets/carpet/scripts/camera.sc
+++ b/src/main/resources/assets/carpet/scripts/camera.sc
@@ -49,6 +49,11 @@ __command() ->
    print(' "/camera show": Show current path particles');
    print('    color of particles used is different for different players');
    print(' "/camera hide": Hide path display');
+   print(' "/camera prefer_smooth_play": ');
+   print('    Eat CPU spikes and continue as usual');
+   print(' "/camera prefer_synced_play": ');
+   print('    After CPU spikes jump to where you should be');
+
    print('');
    ''
 );
@@ -483,6 +488,11 @@ hide() ->
 );
 
 // runs the player on the path
+global_prefer_sync = false;
+
+prefer_smooth_play() -> (global_prefer_sync = false; 'Smooth path play');
+prefer_synced_play() -> (global_prefer_sync = true; 'Synchronized path play');
+
 play() ->
 (
    if (err = __is_not_valid_for_motion(), exit(err));
@@ -501,6 +511,7 @@ play() ->
        global_playing_path = true;
        mspt = 1000 / 60;
        start_time = time();
+       very_start = start_time;
        point = 0;
        p = __get_player();
        try (
@@ -514,9 +525,14 @@ play() ->
                    modify(p, 'location', v);
                    point += 1;
                    end_time = time();
-                   took = end_time - start_time;
-                   if (took < mspt, sleep(mspt-took));
-                   start_time = time()
+                   if (global_prefer_sync,
+                       should_be = very_start + mspt*point;
+                       if (end_time < should_be, sleep(should_be-end_time) )
+                   ,
+                       took = end_time - start_time;
+                       if (took < mspt, sleep(mspt-took));
+                       start_time = time()
+                   )
                )
            );
        );
diff --git a/src/main/resources/assets/carpet/scripts/chunk_display.sc b/src/main/resources/assets/carpet/scripts/chunk_display.sc
new file mode 100644
index 00000000..1eca80e5
--- /dev/null
+++ b/src/main/resources/assets/carpet/scripts/chunk_display.sc
@@ -0,0 +1,179 @@
+global_chunk_colors = m(
+    // loaded ticking
+	l(3,                   l(0xAAdd0000,  0x66990000)), // lime, green
+	l(2,                   l(0xFF000000,  0x88000000)), // red
+	l(1,                   l(0xFFcc0000,  0xbbaa0000)), // yellowish
+	// 'unloaded' (in memory, so rather 'inaccessible')
+	l(0,                   l(0xcccccc00,  0x11111100)), // gray / hi constast
+	// stable full generation (for '0's, all 1,2,3 are always 'full')
+	l('full',              l(0xbbbbbb00,  0x88888800)), // gray
+	// unstable final bits
+	l('heightmaps',        l(0x33333300,  0x22222200)), // checker
+	l('spawn',             l(0x00333300,  0x00222200)), // checker
+	l('light_gen',         l(0x55550000,  0x44440000)), // checker
+	// stable features
+	l('features',          l(0x88bb2200,    0x66882200)), // green muddled
+	//stable terrain
+	l('liquid_carvers',    l(0x65432100,     0x54321000)), // browns
+	//unstable terrain - not generated yet
+	l('carvers',            l(0x33003300,  0x22002200)), // checker
+	l('surface',            l(0x33330000,  0x22220000)), // checker
+	l('noise',              l(0x33000000,  0x22000000)), // checker
+	l('biomes',             l(0x00330000,  0x00220000)), // checker
+	l('structure_references', l(0x00003300,  0x00002200)), // checker
+	// stable
+	l('structure_starts',  l(0x66666600,  0x22222200)), // darkgrey
+	// not stated yet
+	l('empty',             l(0xFF888800,  0xDD444400)), // pink
+	// proper not in memory
+	l(null ,               l(0xFFFFFF00,  0xFFFFFF00)), // hwite
+
+	// ticket types
+	l('player',            l(0x0000DD00,  0x00008800)), // blue
+	l('portal',            l(0xAA00AA00,  0xAA00AA00)), // purple
+	l('dragon',            l(0xAA008800,  0xAA008800)), // purple
+	l('forced',            l(0xAABBFF00,  0x8899AA00)), // blue
+	l('light',             l(0xFFFF0000,  0xBBBB0000)), // yellow
+	l('post_teleport',     l(0xAA00AA00,  0xAA00AA00)), // purple
+	l('start',             l(0xDDFF0000,  0xDDFF0000)), // lime, green
+	// recent chunk requests
+	l('unknown',           l(0xFF55FF00,  0xff99ff00)) // pink purple
+);
+
+// map representing current displayed chunkloading setup
+global_current_setup = null;
+
+// which blocks in the inventory represent which dimension
+global_block_markers = m(
+	l('netherrack', 'the_nether'),
+	l('grass_block', 'overworld'),
+	l('end_stone', 'the_end')
+);
+
+__on_player_uses_item(player, item_tuple, hand) ->
+(
+	if (hand != 'mainhand', return());
+	if (!has(global_block_markers, item_tuple:0), return());
+	if (item_tuple:1%16 != 0, return());
+	new_setup = true;
+	if (global_current_setup,
+		new_setup = __remove_previous_setup(player)
+	);
+	if (!new_setup, return());
+	tag = nbt('{}');
+	put(tag:'Enchantments','[]');
+	put(tag:'Enchantments', '{lvl:1s,id:"minecraft:protection"}', 0);
+	inventory_set(player, player~'selected_slot', item_tuple:1, item_tuple:0, tag);
+	__setup_tracker(player, item_tuple);
+);
+
+__remove_previous_setup(new_player) ->
+(
+	if (!global_current_setup, return());
+	global_running = false;
+	p = player();
+	if (p &&
+			global_block_markers:((item = inventory_get(p, global_current_setup:'inventory_slot')):0)==global_current_setup:'source_dimension' &&
+			item:1 == global_current_setup:'radius' &&
+			item:2,
+		inventory_set(p, global_current_setup:'inventory_slot', item:1, item:0, null); // disenchant
+		if( new_player == p && new_player~'selected_slot' == global_current_setup:'inventory_slot',
+			disabled = true
+		);
+	);
+	global_current_setup = null;
+	!disabled
+);
+
+__setup_tracker(player, item) ->
+(
+	setup = m();
+	setup:'player_name' = str(player);
+	setup:'inventory_slot' = player ~ 'selected_slot';
+	setup:'plot_dimension' = player ~ 'dimension';
+	setup:'plot_center' = map(pos(player), floor(_))-l(0,1,0);
+	setup:'radius' = item:1;
+	setup:'source_dimension' = global_block_markers:(item:0);
+	multiplier = 1;
+	if (setup:'source_dimension' != setup:'plot_dimension',
+		multiplier = if (
+			setup:'source_dimension' == 'the_nether', 1/8,
+			setup:'source_dimension' == 'the_end', 0,
+			8);
+	);
+	setup:'source_center' = setup:'plot_center' * multiplier;
+	print(setup:'source_dimension'+' around '+setup:'source_center'+' with '+setup:'radius'+' radius ('+ (2*setup:'radius'+1)^2 +' chunks)');
+
+	l(sx, sy, sz) = setup:'source_center';
+	global_status_cache = m();
+	loop( 2*setup:'radius'+1, dx = _ - setup:'radius';
+		loop( 2*setup:'radius'+1, dz = _ - setup:'radius';
+			source_pos = l(sx+16*dx,sy,sz+16*dz);
+			global_status_cache:source_pos = l(null, 0);
+		)
+	);
+	global_current_setup = setup;
+	global_running = true;
+	schedule(0, '__chunk_visualizer_tick', player);
+);
+
+global_status_cache = m();
+
+__chunk_visualizer_tick(p) ->
+(
+	setup = global_current_setup;
+	if(!setup, return());
+	if (!global_running, return());
+	show_activity = (p~'holds':0 == 'redstone_torch');
+	yval = setup:'plot_center':1+16;
+	in_dimension( setup:'plot_dimension',
+		l(sx, sy, sz) = setup:'source_center';
+		l(px, py, pz) = setup:'plot_center';
+		source_center = setup:'source_center';
+		radius = setup:'radius';
+		source_dimension = setup:'source_dimension';
+
+        shapes = l();
+        now = tick_time();
+
+		loop( 2*radius+1, dx = _ - radius;
+			loop( 2*radius+1, dz = _ - radius;
+				changed = false;
+				source_pos = l(sx+16*dx,sy,sz+16*dz);
+				status = in_dimension( source_dimension,
+					loaded_status = loaded_status(source_pos);
+					if (loaded_status > 0, loaded_status, generation_status(source_pos))
+				);
+				// will mix with light ticket
+				if (status=='light', status = 'light_gen');
+
+
+				if (loaded_status > 0,
+					tickets = in_dimension( source_dimension, chunk_tickets(source_pos));
+					for(tickets,
+						l(type, level) = _;
+						if (show_activity || type != 'unknown',
+							status = type;
+						);
+					);
+				);
+				l(cached_status, expiry) = global_status_cache:source_pos;
+				changed = (status != cached_status);
+				if ( changed || (expiry < now),
+                    global_status_cache:source_pos = l(status, now+100+floor(rand(40)));
+					bpos = l(dx/2, yval, dz/2);
+					bcol = global_chunk_colors:status:((dx+dz)%2);
+					shapes += l('box', 150, 'from', bpos, 'to', bpos + l(0.5,0,0.5),
+					    'color', 0xffffff00, 'fill', bcol+128, 'follow', p, 'snap', 'xz');
+					if (changed,
+					    pbcol = global_chunk_colors:cached_status:((dx+dz)%2);
+					    shapes += l('box', 0, 'from', bpos, 'to', bpos + l(0.5,0,0.5),
+                            'color', 0xffffff00, 'fill', pbcol+128, 'follow', p, 'snap', 'xz');
+					);
+				);
+			);
+		);
+		draw_shape(shapes);
+	);
+	schedule(1, '__chunk_visualizer_tick', p)
+)
\ No newline at end of file
diff --git a/src/main/resources/assets/carpet/scripts/event_test.sc b/src/main/resources/assets/carpet/scripts/event_test.sc
index 80e2346e..1659d565 100644
--- a/src/main/resources/assets/carpet/scripts/event_test.sc
+++ b/src/main/resources/assets/carpet/scripts/event_test.sc
@@ -185,3 +185,62 @@ __on_player_places_block(player, item_tuple, hand, block) ->
     print('    > count: '+count);
     print('    > nbt: '+nbt);
 );
+
+__on_player_takes_damage(player, amount, source, entity) ->
+(
+    print('');
+    print('__on_player_takes_damage(player, amount, source, source_entity)');
+    print('player takes damage:');
+    print('  - player: '+player);
+    print('  - amount: '+str('%.2f',amount));
+    print('  - source: '+source);
+    print('  - source_entity: '+entity);
+);
+
+__on_player_deals_damage(player, amount, entity) ->
+(
+    print('');
+    print('__on_player_deals_damage(player, amount, target)');
+    print('player deals damage:');
+    print('  - player: '+player);
+    print('  - amount: '+str('%.2f',amount));
+    print('  - target: '+entity);
+);
+
+__on_player_dies(player) ->
+(
+    print('');
+    print('__on_player_dies(player)');
+    print('Player '+player+' dies.')
+);
+
+__on_player_respawns(player) ->
+(
+    print('');
+    print('__on_player_respawns(player)');
+    print('Player '+player+' respawns.')
+);
+
+__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension) ->
+(
+    print('');
+    print('__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension)');
+    print('player changes dimensions:');
+    print('  - player: '+player);
+    print('  - from '+from_dimension+' at '+map(from_pos, str('%.2f',_)));
+    print('  - to '+to_dimension+if(to_pos == null, '', ' at '+map(to_pos, str('%.2f',_))));
+);
+
+__on_player_connects(player) ->
+( // you will never sees it
+    print('');
+    print('__on_player_connects(player)');
+    print('Player '+player+' connects.')
+);
+
+__on_player_disconnects(player, reason) ->
+( // you will never sees it either
+    print('');
+    print('__on_player_disconnects(player)');
+    print('Player '+player+' disconnects because: '+reason)
+);
diff --git a/src/main/resources/assets/carpet/scripts/overlay.sc b/src/main/resources/assets/carpet/scripts/overlay.sc
index 290a51f1..8038b9d3 100644
--- a/src/main/resources/assets/carpet/scripts/overlay.sc
+++ b/src/main/resources/assets/carpet/scripts/overlay.sc
@@ -6,13 +6,13 @@ global_renderers = m(
         l('handler', '__structure_renderer'),
         l('tasks', m()),
         l('range', 3),
-        l('max_pieces', 4)
+        l('max_pieces', 6)
     )),
     l('chunks', m(
         l('active', false),
         l('handler', '__chunk_renderer'),
         l('tasks', m()),
-        l('range', 4)
+        l('range', 6)
     )),
     l('shapes', m(
         l('active', false),
@@ -89,22 +89,25 @@ __structure_renderer(player_name) ->
 		for (starts, l(position, name) = _;
 		    structure_data = structures(position):name;
 		    if (!structure_data, continue()); // messed up references - shouldn't happen
-		    structure_pieces = structures(position, name);
+		    structure_pieces = structures(position, name):'pieces';
 		    if (!structure_pieces, continue()); // messed up references - shouldn't happen
 		    l(from, to) = structure_data;
 		    total_size = _euclidean(from, to);
 		    density = max(10, total_size/10);
-		    particle_rect('dust 0 1 1 2', from, to+1, density);
+		    draw_shape('box', 15, 'from', from, 'to', to+1, 'color', 0x00FFFFFF, 'line', 3, 'fill', 0x00FFFF22);
 		    structure_pieces = slice(sort_key(structure_pieces, _euclidean_sq((_:2+_:3)/2,ppos)), 0, config:'max_pieces');
 		    for (structure_pieces, l(piece, direction, from, to) = _;
-		        factor = 1- _i / config:'max_pieces' / 2;
-		        total_size = _euclidean(from, to);
-                density = max(_i+1, total_size/10);
-		        particle_rect(str('dust %.2f %.2f 1 1',factor, factor), from, to+1, density);
+		        r = 255 - floor(128 * _i/config:'max_pieces');
+		        g = r;
+		        b = 255;
+		        a = 255;
+		        color = 256*(b+256*(g+256*r)); // leaving out a
+		        draw_shape('box', 15, 'from', from, 'to', to+1, 'color', color+a);
+		        draw_shape('box', 15, 'from', from, 'to', to+1, 'color', 0xffffff00, 'fill', 0xffffff22);
 		    )
 		)
 	);
-	schedule(3, '__structure_renderer', player_name);
+	schedule(10, '__structure_renderer', player_name);
 );
 
 __chunk_renderer(player_name) ->
@@ -115,28 +118,32 @@ __chunk_renderer(player_name) ->
         // get lower corner of the chunk
         ppos = map(pos(p)/16, floor(_))*16;
         ppos:1 = 0;
-
-        r = config:'range';
-        for(range(-r,r), cx =_;
-        	for (range(-r,r), cz = _;
+        rang = config:'range';
+        for(range(-rang,rang), cx =_;
+        	for (range(-rang,rang), cz = _;
         	    ref_pos = ppos + l(16*cx,0,16*cz);
                 if(has(config:'tasks':'slime_chunks') && in_slime_chunk(ref_pos),
-                    player_distance = _euclidean(ppos, ref_pos+8);
+                    player_distance = _euclidean(ppos, ref_pos);
                     top_00 = ref_pos + l(0, top('terrain', ref_pos)+10, 0);
                     top_11 = ref_pos + l(16, top('terrain', ref_pos+l(15,0,15))+10, 16);
                     top_10 = ref_pos + l(16, top('terrain', ref_pos+l(15, 0, 0))+10, 0);
                     top_01 = ref_pos + l(0, top('terrain', ref_pos+l(0, 0, 15))+10, 16);
-                    part = 'dust 0.2 0.8 0.2 2';
-                    density = 2+player_distance/2;
-                    particle_line(part, top_00, top_10, density);
-                    particle_line(part, top_10, top_11, density);
-                    particle_line(part, top_11, top_01, density);
-                    particle_line(part, top_01, top_00, density);
-                    particle_line(part, top_00, top_11, density);
-                    particle_line(part, top_01, top_10, density);
+                    r = 30;
+                    g = 220;
+                    b = 30;
+                    a = max(0, 255-player_distance);
+                    color = a+256*(b+256*(g+256*r));
+                    draw_shape(l(
+                    l('line', 15, 'from', top_00, 'to', top_10, 'color', color, 'line', 3),
+                    l('line', 15, 'from', top_10, 'to', top_11, 'color', color, 'line', 3),
+                    l('line', 15, 'from', top_11, 'to', top_01, 'color', color, 'line', 3),
+                    l('line', 15, 'from', top_01, 'to', top_00, 'color', color, 'line', 3),
+                    l('line', 15, 'from', top_00, 'to', top_11, 'color', color, 'line', 3),
+                    l('line', 15, 'from', top_01, 'to', top_10, 'color', color, 'line', 3)
+                    ));
                 )
         	)
         )
     );
-    schedule(3, '__chunk_renderer', player_name);
+    schedule(10, '__chunk_renderer', player_name);
 )
\ No newline at end of file
diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json
index 1ecd3f9b..c527a733 100644
--- a/src/main/resources/carpet.mixins.json
+++ b/src/main/resources/carpet.mixins.json
@@ -6,6 +6,9 @@
   "mixins": [
     "CommandManagerMixin",
 
+    "ServerPlayNetworkHandlerMixin",
+    "CustomPayloadC2SPacketMixin",
+
     "FillCommandMixin",
     "CloneCommandMixin",
     "SetBlockCommandMixin",
@@ -25,11 +28,15 @@
     "ServerWorld_tickMixin",
     "ServerChunkManager_tickMixin",
     "ThreadedAnvilChunkStorage_tickMixin",
+    "ChunkTicketManager_tickMixin",
 
     "ThreadedAnvilChunkStorage_scarpetChunkCreationMixin",
     "ChunkHolder_scarpetChunkCreationMixin",
     "ServerLightingProvider_scarpetChunkCreationMixin",
 
+    "ChunkTicketManager_spawnChunksMixin",
+    "ServerWorld_spawnChunksMixin",
+
     "MobEntityMixin",
     "ServerWorld_unloadedEntityMixin",
     "EntityTrackerEntry_leashFixMixin",
@@ -61,6 +68,8 @@
     "CoralFeatureMixin",
     "StructureFeatureMixin",
     "World_scarpetPlopMixin",
+    "ChunkRegion_scarpetPlopMixin",
+    "StructurePiece_scarpetPlopMixin",
 
     "ServerPlayNetworkHandler_scarpetEventsMixin",
     "ServerPlayerInteractionManager_scarpetEventsMixin",
@@ -141,19 +150,24 @@
     "ServerPlayerEntity_scarpetEventMixin",
     "FallingBlockEntity_scarpetEventsMixin",
     "BlockItem_scarpetEventMixin",
+    "PlayerManager_scarpetEventsMixin",
 
     "ChunkTicketManager_scarpetMixin",
-    "ServerChunkManager_scarpetMixin",
     "StatType_scarpetMixin",
     "PointOfInterest_scarpetMixin",
     "ServerWorld_scarpetEventMixin",
-
+    "HungerManagerMixin_scarpetEventsMixin",
 
     "PistonBlock_movableTEMixin",
     "PistonBlockEntity_movableTEMixin",
     "World_movableTEMixin",
     "WorldChunk_movableTEMixin",
-    "PistonHandler_movableTEMixin"
+    "PistonHandler_movableTEMixin",
+    "PlayerEntity_creativeNoClipMixin",
+    "PistonBlockEntity_creativeNoClipMixin",
+    "BlockItem_creativeNoClipMixin",
+    "LivingEntity_creativeFlyMixin"
+
   ],
   "client": [
     "RenderTickCounter_tickSpeedMixin",
@@ -165,7 +179,12 @@
     "PlayerListHudMixin",
     "PistonBlockEntityRenderer_movableTEMixin",
     "ClientPlayNetworkHandler_smoothClientAnimationsMixin",
-    "ClientPlayerEntity_clientCommandMixin"
+    "ClientPlayerEntity_clientCommandMixin",
+    "WorldRenderer_fogOffMixin",
+    "WorldRenderer_creativeNoClipMixin",
+    "ClientPlayNetworkHandlerMixin",
+    "DebugRenderer_scarpetRenderMixin",
+    "WorldRenderer_scarpetRenderMixin"
   ],
   "server": [
     "MinecraftDedicatedServerMixin"
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index e0f72738..b31f6f4c 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.3.17",
+  "version": "1.3.29",
 
   "name": "Carpet Mod in Fabric",
   "description": "Carpet made out of fabric",