Merge branch 'master' into more-throwables
This commit is contained in:
36 files changed
+609
-128
No files matched your search
@@ -398,6 +398,83 @@ 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`.
|
||||
|
||||
### `create_datapack(name, data)`
|
||||
|
||||
Creates and loads custom datapack. The data has to be a map representing the file structure and the content of the
|
||||
json files of the target pack.
|
||||
|
||||
Returns `null` if the pack with this name already exists or is loaded, meaning no change has been made.
|
||||
Returns `false` if adding of the datapack wasn't successful.
|
||||
Returns `true` if creation and loading of the datapack was successful. Loading of a datapack results in
|
||||
reloading of all other datapacks (vanilla restrictions, identical to /datapack enable), however unlike with `/reload`
|
||||
command, scarpet apps will not be reloaded by adding a datapack using `create_datapack`.
|
||||
|
||||
Currently, only json files are supported in the packs. `'pack.mcmeta'` file is added automatically.
|
||||
|
||||
Reloading of datapacks that define new dimensions is not implemented in vanilla. Vanilla game only loads
|
||||
dimension information on server start. `create_datapack` is therefore a direct replacement of manually ploping of the specified
|
||||
file structure in a datapack file and calling `/datapack enable` on the new datapack with all its quirks and sideeffects
|
||||
(like no worldgen changes, reloading all other datapacks, etc.). To enable newly added custom dimensions, call much more
|
||||
experimental `check_hidden_dimensions()` after adding a datapack if needed.
|
||||
|
||||
Synopsis:
|
||||
<pre>
|
||||
script run create_datapack('foo',
|
||||
{
|
||||
'foo' -> {
|
||||
'bar.json' -> {
|
||||
'c' -> true,
|
||||
'd' -> false,
|
||||
'e' -> {'foo' -> [1,2,3]},
|
||||
'a' -> 'foobar',
|
||||
'b' -> 5
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
script run create_datapack('funky_world', {
|
||||
'data' -> {
|
||||
'minecraft' -> {
|
||||
'dimension' -> {
|
||||
'custom_ow.json' -> {
|
||||
'type' -> 'minecraft:the_end',
|
||||
'generator' -> {
|
||||
'biome_source' -> {
|
||||
'seed' -> 0,
|
||||
'large_biomes' -> false,
|
||||
'type' -> 'minecraft:vanilla_layered'
|
||||
},
|
||||
'seed' -> 0,
|
||||
'settings' -> 'minecraft:nether',
|
||||
'type' -> 'minecraft:noise'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
check_hidden_dimensions(); => ['funky_world']
|
||||
|
||||
</pre>
|
||||
|
||||
### `enable_hidden_dimensions()`
|
||||
|
||||
The function reads current datapack settings detecting new dimensions defined by these datapacks that have not yet been added
|
||||
to the list of current dimensions and adds them so that they can be used and accessed right away. It doesn't matter how the
|
||||
datapacks have been added to the game, either with 'create_datapack()' or manually by dropping a datapack file and calling
|
||||
`/datapack enable` on it. Returns the list of valid dimension names / identifiers that has been added in the process.
|
||||
|
||||
Fine print: The function should be
|
||||
considered experimental. There 'should not be' (famous last words) any side-effects if no worlds are added. Already connected
|
||||
clients will not see suggestions for commands that use dimensions `/execute in <dim>` (vanilla client limitation)
|
||||
but all commands should work just fine with
|
||||
the new dimensions. Existing worlds that have gotten modified settings by the datapacks will not be reloaded or replaced.
|
||||
The usability of the dimensions added this way has not been fully tested, but it seems it works just fine. Generator settings
|
||||
for the new dimensions will not be added to `'level.dat'` but it will be added there automatically next time the game restarts by
|
||||
vanilla. One could have said to use this method with caution, and the authors take no responsibility of any losses incurred due to
|
||||
mis-handlilng of the temporary added dimensions, yet the feature itself (custom dimensions) is clearly experimental for Mojang
|
||||
themselves, so that's about it.
|
||||
|
||||
### `tick_time()`
|
||||
|
||||
Returns server tick counter. Can be used to run certain operations every n-th ticks, or to count in-game time.
|
||||
@@ -517,6 +594,7 @@ Available options in the scarpet app space:
|
||||
Relevant world related properties
|
||||
* `world_name` - name of the world
|
||||
* `world_seed` - a numeric seed of the world
|
||||
* `world_dimensions` - a list of dimensions in the world
|
||||
* `world_path` - full path to the world saves folder
|
||||
* `world_folder` - name of the direct folder in the saves that holds world files
|
||||
* `world_carpet_rules` - returns all Carpet rules in a map form (`rule`->`value`). Note that the values are always returned as strings, so you can't do boolean comparisons directly. Includes rules from extensions with their namespace (`namespace:rule`->`value`). You can later listen to rule changes with the `on_carpet_rule_changes(rule, newValue)` event.
|
||||
|
||||
@@ -792,7 +792,9 @@ All generated structures will retain their properties, like mob spawning, howeve
|
||||
itself has certain rules to spawn mobs, like plopping a nether fortress in the overworld will not spawn nether mobs,
|
||||
because nether mobs can spawn only in the nether, but plopped in the nether - will behave like a valid nether fortress.
|
||||
|
||||
### `custom_dimension(name, seed?)`
|
||||
### (deprecated) `custom_dimension(name, seed?)`
|
||||
|
||||
Deprecated by `create_datapack()` which can be used to setup custom dimensions
|
||||
|
||||
Ensures the dimension with the given `'name'` is available and configured with the given seed. It merely sets the world
|
||||
generator settings to the overworld, and the optional custom seed (or using current world seed, if not provided).
|
||||
|
||||
@@ -177,7 +177,7 @@ Returns `true` if en entity is standing on firm ground and falling down due to t
|
||||
|
||||
### `query(e, 'name'), query(e, 'display_name'), query(e, 'custom_name'), query(e, 'type')`
|
||||
|
||||
String of entity name
|
||||
String of entity name or formatted text in the case of `display_name`
|
||||
|
||||
<pre>
|
||||
query(e,'name') => Leatherworker
|
||||
|
||||
@@ -23,12 +23,12 @@ In case to simplify the access with nested objects, you can add chain of address
|
||||
than calling it multiple times. `get(get(foo,a),b)` is equivalent to `get(foo, a, b)`, or `foo:a:b`.
|
||||
|
||||
<pre>
|
||||
get(l(range(10)), 5) => 5
|
||||
get(l(range(10)), -1) => 9
|
||||
get(l(range(10)), 10) => 0
|
||||
l(range(10)):93 => 3
|
||||
get([range(10)], 5) => 5
|
||||
get([range(10)], -1) => 9
|
||||
get([range(10)], 10) => 0
|
||||
[range(10)]:93 => 3
|
||||
get(player() ~ 'nbt', 'Health') => 20 // inefficient way to get player health, use player() ~ 'health' instead
|
||||
get(m( l('foo',2), l('bar',3), l('baz',4) ), 'bar') => 3
|
||||
get({ 'foo' -> 2, 'bar' -> 3, 'baz' -> 4 }, 'bar') => 3
|
||||
</pre>
|
||||
|
||||
### `has(container, address, ...), has(lvalue)`
|
||||
@@ -100,13 +100,13 @@ of the existing elements. Use `replace` to remove and replace existing element.
|
||||
and merges keys from `value` with the compound tag under the path
|
||||
|
||||
<pre>
|
||||
a = l(1, 2, 3); put(a, 1, 4); a => [1, 4, 3]
|
||||
a = l(1, 2, 3); put(a, null, 4); a => [1, 2, 3, 4]
|
||||
a = l(1, 2, 3); put(a, 1, 4, 'insert'); a => [1, 4, 2, 3]
|
||||
a = l(1, 2, 3); put(a, null, l(4, 5, 6), 'extend'); a => [1, 2, 3, 4, 5, 6]
|
||||
a = l(1, 2, 3); put(a, 1, l(4, 5, 6), 'extend'); a => [1, 4, 5, 6, 2, 3]
|
||||
a = l(l(0,0,0),l(0,0,0),l(0,0,0)); put(a.1, 1, 1); a => [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
|
||||
a = m(1,2,3,4); put(a, 5, null); a => {1: null, 2: null, 3: null, 4: null, 5: null}
|
||||
a = [1, 2, 3]; put(a, 1, 4); a => [1, 4, 3]
|
||||
a = [1, 2, 3]; put(a, null, 4); a => [1, 2, 3, 4]
|
||||
a = [1, 2, 3]; put(a, 1, 4, 'insert'); a => [1, 4, 2, 3]
|
||||
a = [1, 2, 3]; put(a, null, [4, 5, 6], 'extend'); a => [1, 2, 3, 4, 5, 6]
|
||||
a = [1, 2, 3]; put(a, 1, [4, 5, 6], 'extend'); a => [1, 4, 5, 6, 2, 3]
|
||||
a = [[0,0,0],[0,0,0],[0,0,0]]; put(a:1, 1, 1); a => [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
|
||||
a = {1,2,3,4}; put(a, 5, null); a => {1: null, 2: null, 3: null, 4: null, 5: null}
|
||||
tag = nbt('{}'); put(tag, 'BlockData.Properties', '[1,2,3,4]'); tag => {BlockData:{Properties:[1,2,3,4]}}
|
||||
tag = nbt('{a:[{lvl:3},{lvl:5},{lvl:2}]}'); put(tag, 'a[].lvl', 1); tag => {a:[{lvl:1},{lvl:1},{lvl:1}]}
|
||||
tag = nbt('{a:[{lvl:[1,2,3]},{lvl:[3,2,1]},{lvl:[4,5,6]}]}'); put(tag, 'a[].lvl', 1, 2); tag
|
||||
@@ -187,10 +187,10 @@ other parts. In that case consecutive calls to `slice` will refer to index `0` t
|
||||
cannot go back nor track where they are in the sequence (see examples).
|
||||
|
||||
<pre>
|
||||
slice([0,1,2,3,4,5], 1, 3) => [1, 2, 3]
|
||||
slice([0,1,2,3,4,5], 1, 3) => [1, 2]
|
||||
slice('foobar', 0, 1) => 'f'
|
||||
slice('foobar', 3) => 'bar'
|
||||
slice(range(10), 3, 5) => [3, 4, 5]
|
||||
slice(range(10), 3, 5) => [3, 4]
|
||||
slice(range(10), 5) => [5, 6, 7, 8, 9]
|
||||
r = range(100); [slice(r, 5, 7), slice(r, 1, 3)] => [[5, 6], [8, 9]]
|
||||
</pre>
|
||||
@@ -225,7 +225,7 @@ Primarily to be used in higher order functions
|
||||
|
||||
<pre>
|
||||
range(10) => [...]
|
||||
l(range(10)) => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
[range(10)] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
map(range(10),_*_) => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
|
||||
reduce(range(10),_a+_, 0) => 45
|
||||
range(5,10) => [5, 6, 7, 8, 9]
|
||||
|
||||
@@ -124,12 +124,12 @@ to `all(list,expr) <=> for(list,expr)==length(list)`. `expr` also receives bound
|
||||
and `continue` have no sense and cannot be used inside of `expr` body.
|
||||
|
||||
<pre>
|
||||
all([1,2,3], check_prime(_)) => 1
|
||||
all(neighbours(x,y,z), _=='stone') => 1 // if all neighbours of [x, y, z] are stone
|
||||
map(filter(rect(0,4,0,1000,0,1000), l(x,y,z)=pos(_); all(rect(x,y,z,1,0,1),_=='bedrock') ), pos(_) )
|
||||
all([1,2,3], check_prime(_)) => true
|
||||
all(neighbours(x,y,z), _=='stone') => true // if all neighbours of [x, y, z] are stone
|
||||
map(filter(rect(0,4,0,1000,0,1000), [x,y,z]=pos(_); all(rect(x,y,z,1,0,1),_=='bedrock') ), pos(_) )
|
||||
=> [[-298, 4, -703], [-287, 4, -156], [-269, 4, 104], [242, 4, 250], [-159, 4, 335], [-208, 4, 416], [-510, 4, 546], [376, 4, 806]]
|
||||
// find all 3x3 bedrock structures in the top bedrock layer
|
||||
map( filter( rect(0,4,0,1000,1,1000,1000,0,1000), l(x,y,z)=pos(_);
|
||||
map( filter( rect(0,4,0,1000,1,1000,1000,0,1000), [x,y,z]=pos(_);
|
||||
all(rect(x,y,z,1,0,1),_=='bedrock') && for(rect(x,y-1,z,1,1,1,1,0,1),_=='bedrock')<8),
|
||||
pos(_) ) => [[343, 3, -642], [153, 3, -285], [674, 3, 167], [-710, 3, 398]]
|
||||
// ditto, but requiring at most 7 bedrock block in the 18 blocks below them
|
||||
|
||||
@@ -16,7 +16,7 @@ in [User Defined Functions and Program Control Flow](docs/scarpet/language/Funct
|
||||
1357-5 => 1352
|
||||
1357-'5' => 137
|
||||
3*'foo'-'o' => 'fff'
|
||||
l(1,3,5)+7 => l(8,10,12)
|
||||
[1,3,5]+7 => [8,10,12]
|
||||
</pre>
|
||||
|
||||
As you can see, values can behave differently when mixed with other types in the same expression.
|
||||
@@ -76,8 +76,8 @@ In Minecraft API portion `entity ~ feature` is a shortcode for `query(entity,fea
|
||||
any extra arguments.
|
||||
|
||||
<pre>
|
||||
l(1,2,3) ~ 2 => 1
|
||||
l(1,2,3) ~ 4 => null
|
||||
[1,2,3] ~ 2 => 1
|
||||
[1,2,3] ~ 4 => null
|
||||
|
||||
'foobar' ~ 'baz' => null
|
||||
'foobar' ~ '.b' => 'ob'
|
||||
@@ -103,10 +103,10 @@ Or an example to find if a player has specific enchantment on a held axe (either
|
||||
|
||||
<pre>
|
||||
global_get_enchantment(p, ench) -> (
|
||||
$ for(l('main','offhand'),
|
||||
$ for(['main','offhand'],
|
||||
$ holds = query(p, 'holds', _);
|
||||
$ if( holds,
|
||||
$ l(what, count, nbt) = holds;
|
||||
$ [what, count, nbt] = holds;
|
||||
$ if( what ~ '_axe' && nbt ~ ench,
|
||||
$ lvl = max(lvl, number(nbt ~ '(?<=lvl:)\\d') )
|
||||
$ )
|
||||
@@ -143,8 +143,8 @@ Examples:
|
||||
'foo'*3 => 'foofoofoo'
|
||||
'foofoofoo' / 3 => 'foo'
|
||||
'foofoofoo'-'o' => 'fff'
|
||||
l(1,2,3)+1 => l(2,3,4)
|
||||
b = l(100,63,100); b+l(10,0,10) => l(110,63,110)
|
||||
[1,2,3]+1 => [2,3,4]
|
||||
b = [100,63,100]; b+[10,0,10] => [110,63,110]
|
||||
{'a' -> 1} + {'b' -> 2} => {'a' -> 1, 'b' -> 2}
|
||||
</pre>
|
||||
|
||||
@@ -186,11 +186,12 @@ second operand is not necessary, it won't be evaluated, which means one can use
|
||||
case of success returns first positive operand (`||`) or last one (`&&`).
|
||||
|
||||
<pre>
|
||||
true || false => 1
|
||||
null || false => 0
|
||||
null != false || run('kill gnembon') => 1 // gnembon survives
|
||||
null != false && run('kill gnembon') => 0 // when cheats not allowed
|
||||
null != false && run('kill gnembon') => 1 // gnembon dies, cheats allowed
|
||||
true || false => true
|
||||
null || false => false
|
||||
false || null => null
|
||||
null != false || run('kill gnembon') // gnembon survives
|
||||
null != false && run('kill gnembon') // when cheats not allowed
|
||||
null != false && run('kill gnembon') // gnembon dies, cheats allowed
|
||||
</pre>
|
||||
|
||||
### `Assignment Operators = <> +=`
|
||||
@@ -204,11 +205,11 @@ require rewriting of the array anyways.
|
||||
|
||||
<pre>
|
||||
a = 5 => a == 5
|
||||
l(a,b,c) = l(3,4,5) => a==3, b==4, c==5
|
||||
l(minx,maxx) = sort(xi,xj); // minx assumes min(xi, xj) and maxx, max(xi, xj)
|
||||
l(a,b,c,d,e,f) = l(range(6)); l(a,b,c) <> l(d,e,f); l(a,b,c,d,e,f) => [3,4,5,0,1,2]
|
||||
a = l(1,2,3); a += 4 => [1,2,3,4]
|
||||
a = l(1,2,3,4); a = filter(a,_!=2) => [1,3,4]
|
||||
[a,b,c] = [3,4,5] => a==3, b==4, c==5
|
||||
[minx,maxx] = sort(xi,xj); // minx assumes min(xi, xj) and maxx, max(xi, xj)
|
||||
[a,b,c,d,e,f] = [range(6)]; [a,b,c] <> [d,e,f]; [a,b,c,d,e,f] => [3,4,5,0,1,2]
|
||||
a = [1,2,3]; a += 4 => [1,2,3,4]
|
||||
a = [1,2,3,4]; a = filter(a,_!=2) => [1,3,4]
|
||||
</pre>
|
||||
|
||||
### `Unary Operators - +`
|
||||
@@ -226,10 +227,10 @@ Require a number, flips the sign. One way to assert it's a number is by crashing
|
||||
flips boolean condition of the expression. Equivalent of `bool(expr)==false`
|
||||
|
||||
<pre>
|
||||
!true => 0
|
||||
!false => 1
|
||||
!null => 1
|
||||
!5 => 0
|
||||
!l() => 1
|
||||
!l(null) => 0
|
||||
!true => false
|
||||
!false => true
|
||||
!null => true
|
||||
!5 => false
|
||||
!l() => true
|
||||
!l(null) => false
|
||||
</pre>
|
||||
@@ -14,30 +14,31 @@ as well as minecraft related concepts like `block`, `entity`, `nbt`, `text`.
|
||||
|
||||
### `bool(expr)`
|
||||
|
||||
Returns a boolean context of the expression. Note that there are no true/false values in scarpet. `true` is
|
||||
alias of 1, and `false` is 0\. Bool is also interpreting string values as boolean, which is different from other
|
||||
Returns a boolean context of the expression.
|
||||
Bool is also interpreting string values as boolean, which is different from other
|
||||
places where boolean context can be used. This can be used in places where API functions return string values to
|
||||
represent binary values
|
||||
represent binary values.
|
||||
|
||||
<pre>
|
||||
bool(pi) => 1
|
||||
bool(false) => 0
|
||||
bool('') => 0
|
||||
bool(l()) => 0
|
||||
bool(l('')) => 1
|
||||
bool('foo') => 1
|
||||
bool('false') => 0
|
||||
bool('nulL') => 0
|
||||
if('false',1,0) => 1
|
||||
bool(pi) => true
|
||||
bool(false) => false
|
||||
bool('') => false
|
||||
bool(l()) => false
|
||||
bool(l('')) => true
|
||||
bool('foo') => true
|
||||
bool('false') => false
|
||||
bool('nulL') => false
|
||||
if('false',1,0) => true
|
||||
</pre>
|
||||
|
||||
### `number(expr)`
|
||||
|
||||
Returns a numeric context of the expression. Can be used to read numbers from strings
|
||||
Returns a numeric context of the expression. Can be used to read numbers from strings, or other types
|
||||
|
||||
<pre>
|
||||
number(null) => null
|
||||
number(null) => 0
|
||||
number(false) => 0
|
||||
number(true) => 1
|
||||
number('') => null
|
||||
number('3.14') => 3.14
|
||||
number(l()) => 0
|
||||
@@ -65,15 +66,15 @@ Supported types (with `"%<?>"` syntax):
|
||||
* `%%`: '%' character
|
||||
|
||||
<pre>
|
||||
str(null) => null
|
||||
str(false) => 0
|
||||
str('') => null
|
||||
str('3.14') => 3.14
|
||||
str(l()) => 0
|
||||
str(l('')) => 1
|
||||
str('foo') => null
|
||||
str('3bar') => null
|
||||
str(2)+str(2) => 22
|
||||
str(null) => 'null'
|
||||
str(false) => 'false'
|
||||
str('') => ''
|
||||
str('3.14') => '3.14'
|
||||
str([]) => '[]'
|
||||
str(['']) => '[]'
|
||||
str('foo') => 'foo'
|
||||
str('3bar') => '3bar'
|
||||
str(2)+str(2) => '22'
|
||||
str('pi: %.2f',pi) => 'pi: 3.14'
|
||||
str('player at: %d %d %d',pos(player())) => 'player at: 567, -2423, 124'
|
||||
</pre>
|
||||
@@ -234,7 +235,7 @@ same random object. Scarpet keeps track of up to 1024 custom random number gener
|
||||
|
||||
<pre>
|
||||
map(range(10), floor(rand(10))) => [5, 8, 0, 6, 9, 3, 9, 9, 1, 8]
|
||||
map(range(10), bool(rand(2))) => [1, 1, 1, 0, 0, 1, 1, 0, 0, 0]
|
||||
map(range(10), bool(rand(2))) => [false, false, true, false, false, false, true, false, true, false]
|
||||
map(range(10), str('%.1f',rand(_))) => [0.0, 0.4, 0.6, 1.9, 2.8, 3.8, 5.3, 2.2, 1.6, 5.6]
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
`scarpet` provides a number of constants that can be used literally in scripts
|
||||
|
||||
* `null`: nothing, zilch, not even false
|
||||
* `true`: pure true, or just 1 (one)
|
||||
* `false`: false truth, or true falsth, 0 (zero) actually
|
||||
* `true`: pure true, can act as `1`
|
||||
* `false`: false truth, or true falsth, equals to `0`
|
||||
* `pi`: for the fans of perimeters, its a perimeter of an apple pi of diameter 1\. About 3.14
|
||||
* `euler`: clever guy. Derivative of its exponent is goto 1\. About 2.72
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ brightness
|
||||
chunk_tickets
|
||||
convert_date
|
||||
crafting_remaining_item
|
||||
create_datapack
|
||||
create_marker
|
||||
current_dimension
|
||||
custom_dimension
|
||||
day_time
|
||||
delete_file
|
||||
destroy
|
||||
@@ -28,6 +28,7 @@ display_title
|
||||
draw_shape
|
||||
drop_item
|
||||
emitted_light
|
||||
enable_hidden_dimensions
|
||||
encode_nbt
|
||||
entity_area
|
||||
entity_event
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
set without_updates
|
||||
blocks_movement block_sound material map_colour block_state block_list block_tags block_data poi set_poi
|
||||
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 custom_dimension
|
||||
structure_eligibility structures structure_references set_structure reset_chunk reload_chunk create_datapack enable_hidden_dimensions
|
||||
add_chunk_ticket
|
||||
|
||||
convert_date unix_time
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ org.gradle.jvmargs=-Xmx1G
|
||||
fabric_version=0.29.4+1.16
|
||||
|
||||
# Mod Properties
|
||||
mod_version = 1.4.27
|
||||
mod_version = 1.4.28
|
||||
maven_group = carpet
|
||||
archives_base_name = fabric-carpet
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
jcenter()
|
||||
maven {
|
||||
name = 'Fabric'
|
||||
url = 'https://maven.fabricmc.net/'
|
||||
|
||||
@@ -42,7 +42,7 @@ import static carpet.settings.RuleCategory.CLIENT;
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public class CarpetSettings
|
||||
{
|
||||
public static final String carpetVersion = "1.4.27+v210217";
|
||||
public static final String carpetVersion = "1.4.28+v210224";
|
||||
public static final Logger LOG = LogManager.getLogger("carpet");
|
||||
public static boolean skipGenerationChecks = false;
|
||||
public static boolean impendingFillSkipUpdates = false;
|
||||
@@ -413,20 +413,26 @@ public class CarpetSettings
|
||||
|
||||
private static class ModulePermissionLevel extends Validator<String> {
|
||||
@Override public String validate(ServerCommandSource source, ParsedRule<String> currentRule, String newValue, String string) {
|
||||
CarpetSettings.runPermissionLevel = SettingsManager.getCommandLevel(newValue);
|
||||
int permissionLevel = SettingsManager.getCommandLevel(newValue);
|
||||
if (source != null && !source.hasPermissionLevel(permissionLevel))
|
||||
return null;
|
||||
CarpetSettings.runPermissionLevel = permissionLevel;
|
||||
CarpetServer.settingsManager.notifyPlayersCommandsChanged();
|
||||
return newValue;
|
||||
}
|
||||
@Override
|
||||
public String description() { return "Also controls permission level of commands executed via `run()`";}
|
||||
public String description() { return "When changing the rule, you must at least have the permission level you are trying to give it";}
|
||||
}
|
||||
@Rule(
|
||||
desc = "Enables restrictions for arbitrary code execution with scarpet",
|
||||
extra = {
|
||||
"Users that don't have this permission level",
|
||||
"won't be able to load apps or /script run"
|
||||
"won't be able to load apps or /script run.",
|
||||
"It is also the permission level apps will",
|
||||
"have when running commands with run()"
|
||||
},
|
||||
category = {COMMAND, SCARPET},
|
||||
validate = ModulePermissionLevel.class
|
||||
category = {SCARPET},
|
||||
validate = {Validator._COMMAND_LEVEL_VALIDATOR.class, ModulePermissionLevel.class}
|
||||
)
|
||||
public static String commandScriptACE = "ops";
|
||||
|
||||
@@ -532,6 +538,14 @@ public class CarpetSettings
|
||||
)
|
||||
public static int maxEntityCollisions = 0;
|
||||
|
||||
@Rule(
|
||||
desc = "Customizable server list ping (Multiplayer menu) playerlist sample limit",
|
||||
options = {"0", "12", "20", "40"},
|
||||
category = CREATIVE,
|
||||
strict = false,
|
||||
validate = Validator.NONNEGATIVE_NUMBER.class
|
||||
)
|
||||
public static int pingPlayerListLimit = 12;
|
||||
/*
|
||||
|
||||
@Rule(
|
||||
@@ -860,7 +874,7 @@ public class CarpetSettings
|
||||
|
||||
@Rule(
|
||||
desc = "Lightning kills the items that drop when lightning kills an entity",
|
||||
extra = {"Setting to true will prevent lightning from killing drops", "Fixes (https://bugs.mojang.com/browse/MC-195640)."},
|
||||
extra = {"Setting to true will prevent lightning from killing drops", "Fixes [MC-206922](https://bugs.mojang.com/browse/MC-206922)."},
|
||||
category = {BUGFIX}
|
||||
)
|
||||
public static boolean lightningKillsDropsFix = false;
|
||||
|
||||
@@ -23,6 +23,7 @@ import net.minecraft.server.command.ServerCommandSource;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.Vec2f;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
@@ -271,7 +272,13 @@ public class PlayerCommand
|
||||
Messenger.m(context.getSource(), "rb Player name: "+playerName+" is too long");
|
||||
return 0;
|
||||
}
|
||||
|
||||
MinecraftServer server = source.getMinecraftServer();
|
||||
if (!World.isInBuildLimit(new BlockPos(pos.x, pos.y, pos.z)))
|
||||
{
|
||||
Messenger.m(context.getSource(), "rb Player "+playerName+" cannot be placed outside of the world");
|
||||
return 0;
|
||||
}
|
||||
PlayerEntity player = EntityPlayerMPFake.createFake(playerName, server, pos.x, pos.y, pos.z, facing.y, facing.x, dimType, mode);
|
||||
if (player == null)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package carpet.fakes;
|
||||
|
||||
import net.minecraft.resource.ServerResourceManager;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.registry.RegistryKey;
|
||||
import net.minecraft.world.World;
|
||||
@@ -13,4 +14,5 @@ public interface MinecraftServerInterface
|
||||
void forceTick(BooleanSupplier sup);
|
||||
LevelStorage.Session getCMSession();
|
||||
Map<RegistryKey<World>, ServerWorld> getCMWorlds();
|
||||
ServerResourceManager getResourceManager();
|
||||
}
|
||||
@@ -107,20 +107,22 @@ public class OptimizedExplosion
|
||||
|
||||
explosionSound++;
|
||||
|
||||
Entity explodingEntity = eAccess.getEntity();
|
||||
for (int k2 = 0; k2 < entitylist.size(); ++k2) {
|
||||
Entity entity = entitylist.get(k2);
|
||||
|
||||
if (entity == eAccess.getEntity()) {
|
||||
|
||||
if (entity == explodingEntity) {
|
||||
// entitylist.remove(k2);
|
||||
removeFast(entitylist, k2);
|
||||
k2--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entity instanceof TntEntity &&
|
||||
entity.getX() == eAccess.getEntity().getX() &&
|
||||
entity.getY() == eAccess.getEntity().getY() &&
|
||||
entity.getZ() == eAccess.getEntity().getZ()) {
|
||||
if (entity instanceof TntEntity && explodingEntity != null &&
|
||||
entity.getX() == explodingEntity.getX() &&
|
||||
entity.getY() == explodingEntity.getY() &&
|
||||
entity.getZ() == explodingEntity.getZ()) {
|
||||
if (eLogger != null) {
|
||||
eLogger.onEntityImpacted(entity, new Vec3d(0,-0.9923437498509884d, 0));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package carpet.mixins;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.Constant;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyConstant;
|
||||
|
||||
import carpet.CarpetSettings;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
@Mixin(MinecraftServer.class)
|
||||
public abstract class MinecraftServer_pingPlayerSampleLimit
|
||||
{
|
||||
|
||||
@ModifyConstant(method = "tick", constant = @Constant(intValue = 12), require = 0, allow = 1)
|
||||
private int modifyPlayerSampleLimit(int value)
|
||||
{
|
||||
return CarpetSettings.pingPlayerListLimit;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package carpet.mixins;
|
||||
|
||||
import carpet.fakes.MinecraftServerInterface;
|
||||
import carpet.helpers.TickSpeed;
|
||||
import net.minecraft.resource.ServerResourceManager;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.ServerTask;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
@@ -44,6 +45,8 @@ public abstract class MinecraftServer_scarpetMixin extends ReentrantThreadExecut
|
||||
|
||||
@Shadow @Final private Map<RegistryKey<World>, ServerWorld> worlds;
|
||||
|
||||
@Shadow private ServerResourceManager serverResourceManager;
|
||||
|
||||
@Override
|
||||
public void forceTick(BooleanSupplier isAhead)
|
||||
{
|
||||
@@ -58,6 +61,11 @@ public abstract class MinecraftServer_scarpetMixin extends ReentrantThreadExecut
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResourceManager getResourceManager() {
|
||||
return serverResourceManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<RegistryKey<World>, ServerWorld> getCMWorlds()
|
||||
{
|
||||
|
||||
@@ -25,10 +25,4 @@ public class PlayerManager_coreMixin
|
||||
CarpetServer.onPlayerLoggedIn(player);
|
||||
PLAYER_CONNECTS.onPlayerEvent(player);
|
||||
}
|
||||
|
||||
@Inject(method = "onDataPacksReloaded", at = @At("HEAD"))
|
||||
private void onResourcesRefreshedReloadCommand(CallbackInfo ci)
|
||||
{
|
||||
CarpetServer.onReload(server);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package carpet.mixins;
|
||||
|
||||
import carpet.CarpetServer;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.minecraft.server.command.ReloadCommand;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
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;
|
||||
|
||||
@Mixin(ReloadCommand.class)
|
||||
public class ReloadCommand_reloadAppsMixin {
|
||||
//method_13530(Lcom/mojang/brigadier/context/CommandContext;)I
|
||||
// internal of register.
|
||||
@SuppressWarnings("UnresolvedMixinReference")
|
||||
@Inject(method = "method_13530", at = @At("TAIL"))
|
||||
private static void onReload(CommandContext<ServerCommandSource> context, CallbackInfoReturnable<Integer> cir)
|
||||
{
|
||||
// can't fetch here the reference to the server
|
||||
CarpetServer.onReload(context.getSource().getMinecraftServer());
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import net.minecraft.util.WorldSavePath;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
@@ -136,7 +137,8 @@ public class CarpetScriptServer
|
||||
{
|
||||
try {
|
||||
Path folder = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
|
||||
Files.createDirectories(folder);
|
||||
if (!Files.exists(folder))
|
||||
Files.createDirectories(folder);
|
||||
Optional<Path>
|
||||
scriptPath = Files.list(folder)
|
||||
.filter(script ->
|
||||
@@ -149,7 +151,8 @@ public class CarpetScriptServer
|
||||
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
|
||||
{
|
||||
Path globalFolder = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
|
||||
Files.createDirectories(globalFolder);
|
||||
if (!Files.exists(globalFolder))
|
||||
Files.createDirectories(globalFolder);
|
||||
scriptPath = Files.walk(globalFolder)
|
||||
.filter(script -> script.getFileName().toString().equalsIgnoreCase(name + ".sc") ||
|
||||
(allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl")))
|
||||
@@ -194,7 +197,8 @@ public class CarpetScriptServer
|
||||
}
|
||||
try {
|
||||
Path worldScripts = server.getSavePath(WorldSavePath.ROOT).resolve("scripts");
|
||||
Files.createDirectories(worldScripts);
|
||||
if (!Files.exists(worldScripts))
|
||||
Files.createDirectories(worldScripts);
|
||||
Files.list(worldScripts)
|
||||
.filter(f -> f.toString().endsWith(".sc"))
|
||||
.forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT)));
|
||||
@@ -202,8 +206,9 @@ public class CarpetScriptServer
|
||||
if (includeBuiltIns && (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT))
|
||||
{
|
||||
Path globalScripts = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts");
|
||||
Files.createDirectories(globalScripts);
|
||||
Files.walk(globalScripts)
|
||||
if (!Files.exists(globalScripts))
|
||||
Files.createDirectories(globalScripts);
|
||||
Files.walk(globalScripts, FileVisitOption.FOLLOW_LINKS)
|
||||
.filter(f -> f.toString().endsWith(".sc"))
|
||||
.forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$","").toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import carpet.script.exception.InternalExpressionException;
|
||||
import carpet.script.exception.ThrowStatement;
|
||||
import carpet.script.exception.Throwables;
|
||||
import carpet.script.utils.FixedCommandSource;
|
||||
import carpet.script.utils.InputValidator;
|
||||
import carpet.script.utils.ScarpetJsonDeserializer;
|
||||
import carpet.script.utils.ShapeDispatcher;
|
||||
import carpet.script.utils.WorldTools;
|
||||
@@ -37,6 +38,10 @@ import carpet.script.value.StringValue;
|
||||
import carpet.script.value.Value;
|
||||
import carpet.script.value.ValueConversions;
|
||||
import carpet.utils.Messenger;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.command.DataCommandStorage;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -47,12 +52,18 @@ import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtHelper;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.network.packet.s2c.play.PlaySoundIdS2CPacket;
|
||||
import net.minecraft.network.packet.s2c.play.TitleS2CPacket;
|
||||
import net.minecraft.network.packet.s2c.play.TitleS2CPacket.Action;
|
||||
import net.minecraft.particle.ParticleEffect;
|
||||
import net.minecraft.resource.DataPackSettings;
|
||||
import net.minecraft.resource.ReloadableResourceManagerImpl;
|
||||
import net.minecraft.resource.ResourcePackManager;
|
||||
import net.minecraft.resource.ResourcePackProfile;
|
||||
import net.minecraft.resource.ServerResourceManager;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.command.ServerCommandSource;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
@@ -65,13 +76,27 @@ import net.minecraft.text.Text;
|
||||
import net.minecraft.util.Identifier;
|
||||
import net.minecraft.util.InvalidIdentifierException;
|
||||
import net.minecraft.util.crash.CrashException;
|
||||
import net.minecraft.util.Util;
|
||||
import net.minecraft.util.WorldSavePath;
|
||||
import net.minecraft.util.dynamic.RegistryOps;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.ChunkPos;
|
||||
import net.minecraft.util.math.EulerAngle;
|
||||
import net.minecraft.util.math.Vec3d;
|
||||
import net.minecraft.util.registry.DynamicRegistryManager;
|
||||
import net.minecraft.util.registry.Registry;
|
||||
import net.minecraft.util.registry.RegistryKey;
|
||||
import net.minecraft.world.SaveProperties;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.biome.source.BiomeAccess;
|
||||
import net.minecraft.world.border.WorldBorderListener;
|
||||
import net.minecraft.world.dimension.DimensionOptions;
|
||||
import net.minecraft.world.dimension.DimensionType;
|
||||
import net.minecraft.world.gen.GeneratorOptions;
|
||||
import net.minecraft.world.gen.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.UnmodifiableLevelProperties;
|
||||
import net.minecraft.world.level.storage.LevelStorage;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
|
||||
@@ -80,6 +105,13 @@ import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -98,7 +130,7 @@ import static java.lang.Math.min;
|
||||
public class Auxiliary {
|
||||
public static final String MARKER_STRING = "__scarpet_marker";
|
||||
private static final Map<String, SoundCategory> mixerMap = Arrays.stream(SoundCategory.values()).collect(Collectors.toMap(SoundCategory::getName, k -> k));
|
||||
public static final Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().registerTypeAdapter(Value.class, new ScarpetJsonDeserializer()).create();
|
||||
public static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().registerTypeAdapter(Value.class, new ScarpetJsonDeserializer()).create();
|
||||
|
||||
public static String recognizeResource(Value value, boolean isFloder)
|
||||
{
|
||||
@@ -849,7 +881,7 @@ public class Auxiliary {
|
||||
exc = e.getCause();
|
||||
throw new ThrowStatement("Failed to read JSON file: "+exc.getMessage(), Throwables.JSON_READ);
|
||||
}
|
||||
Value parsedJson = gson.fromJson(json, Value.class);
|
||||
Value parsedJson = GSON.fromJson(json, Value.class);
|
||||
if (parsedJson == null)
|
||||
retVal = Value.NULL;
|
||||
else
|
||||
@@ -888,7 +920,7 @@ public class Auxiliary {
|
||||
}
|
||||
else if (fdesc.getMiddle().equals("json"))
|
||||
{
|
||||
List<String> data = Collections.singletonList(gson.toJson(lv.get(2).evalValue(c).toJson()));
|
||||
List<String> data = Collections.singletonList(GSON.toJson(lv.get(2).evalValue(c).toJson()));
|
||||
((CarpetScriptHost) c.host).removeResourceFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle());
|
||||
success = ((CarpetScriptHost) c.host).appendLogFile(fdesc.getLeft(), fdesc.getRight(), fdesc.getMiddle(), data);
|
||||
}
|
||||
@@ -1066,8 +1098,179 @@ public class Auxiliary {
|
||||
if (old_nbt == null) return LazyValue.NULL;
|
||||
return (_c, _t) -> new NBTSerializableValue(old_nbt);
|
||||
});
|
||||
|
||||
// script run create_datapack('foo', {'foo' -> {'bar.json' -> {'c' -> true,'d' -> false,'e' -> {'foo' -> [1,2,3]},'a' -> 'foobar','b' -> 5}}})
|
||||
expression.addLazyFunction("create_datapack", 2, (c, t, lv) -> {
|
||||
CarpetContext cc = (CarpetContext)c;
|
||||
String origName = lv.get(0).evalValue(c).getString();
|
||||
String name = InputValidator.validateSimpleString(origName, true);
|
||||
MinecraftServer server = cc.s.getMinecraftServer();
|
||||
for (String dpName : server.getDataPackManager().getNames())
|
||||
{
|
||||
if (dpName.equalsIgnoreCase("file/"+name+".zip") ||
|
||||
dpName.equalsIgnoreCase("file/"+name))
|
||||
return LazyValue.NULL;
|
||||
|
||||
}
|
||||
Value dpdata = lv.get(1).evalValue(c);
|
||||
if (!(dpdata instanceof MapValue))
|
||||
throw new InternalExpressionException("datapack data needs to be a valid map type");
|
||||
ResourcePackManager packManager = server.getDataPackManager();
|
||||
Path dbFloder = server.getSavePath(WorldSavePath.DATAPACKS);
|
||||
Path packFloder = dbFloder.resolve(name+".zip");
|
||||
if (Files.exists(packFloder) || Files.exists(dbFloder.resolve(name))) return LazyValue.NULL;
|
||||
Boolean [] successful = new Boolean[]{true};
|
||||
server.submitAndJoin( () ->
|
||||
{
|
||||
try {
|
||||
//Files.createDirectory(packFloder);
|
||||
try (FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:" + packFloder.toUri().toString()), ImmutableMap.of("create", "true"))) {
|
||||
Path zipRoot = zipfs.getPath("/");
|
||||
zipValueToJson(zipRoot.resolve("pack.mcmeta"), MapValue.wrap(
|
||||
ImmutableMap.of(StringValue.of("pack"), MapValue.wrap(ImmutableMap.of(
|
||||
StringValue.of("pack_format"), new NumericValue(SharedConstants.getGameVersion().getPackVersion()),
|
||||
StringValue.of("description"), StringValue.of(name),
|
||||
StringValue.of("source"), StringValue.of("scarpet")
|
||||
)))
|
||||
));
|
||||
walkTheDPMap((MapValue) dpdata, zipRoot);
|
||||
}
|
||||
packManager.scanPacks();
|
||||
ResourcePackProfile resourcePackProfile = packManager.getProfile("file/" + name + ".zip");
|
||||
if (resourcePackProfile == null || packManager.getEnabledProfiles().contains(resourcePackProfile)) {
|
||||
throw new IOException();
|
||||
}
|
||||
List<ResourcePackProfile> list = Lists.newArrayList(packManager.getEnabledProfiles());
|
||||
resourcePackProfile.getInitialPosition().insert(list, resourcePackProfile, p -> p, false);
|
||||
|
||||
|
||||
server.reloadResources(list.stream().map(ResourcePackProfile::getName).collect(Collectors.toList())).
|
||||
exceptionally(exc -> {
|
||||
successful[0] = false;
|
||||
return null;
|
||||
}).join();
|
||||
if (!successful[0]) {
|
||||
throw new IOException();
|
||||
}
|
||||
} catch (IOException e)
|
||||
{
|
||||
successful[0] = false;
|
||||
try {
|
||||
FileUtils.forceDelete(packFloder.toFile());
|
||||
} catch (IOException ignored) {
|
||||
throw new InternalExpressionException("Failed to install a datapack and failed to clean up after it");
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
return successful[0]?LazyValue.TRUE:LazyValue.FALSE;
|
||||
});
|
||||
|
||||
expression.addLazyFunction("enable_hidden_dimensions", 0, (c, t, lv) -> {
|
||||
CarpetContext cc = (CarpetContext)c;
|
||||
// from minecraft.server.Main.main
|
||||
MinecraftServer server = cc.s.getMinecraftServer();
|
||||
LevelStorage.Session session = ((MinecraftServerInterface)server).getCMSession();
|
||||
DataPackSettings dataPackSettings = session.getDataPackSettings();
|
||||
ResourcePackManager resourcePackManager = server.getDataPackManager();
|
||||
DataPackSettings dataPackSettings2 = MinecraftServer.loadDataPacks(resourcePackManager, dataPackSettings == null ? DataPackSettings.SAFE_MODE : dataPackSettings, false);
|
||||
ServerResourceManager serverRM = ((MinecraftServerInterface)server).getResourceManager();
|
||||
ReloadableResourceManagerImpl resourceManager = (ReloadableResourceManagerImpl) serverRM.getResourceManager();
|
||||
|
||||
//believe the other one will fillup based on the datapacks only.
|
||||
resourceManager.close();
|
||||
resourcePackManager.createResourcePacks().forEach(resourceManager::addPack);
|
||||
|
||||
//not sure its needed, but doesn't seem to have a negative effect and might be used in some custom shtuff
|
||||
serverRM.loadRegistryTags();
|
||||
|
||||
RegistryOps<Tag> registryOps = RegistryOps.of(NbtOps.INSTANCE, serverRM.getResourceManager(), (DynamicRegistryManager.Impl) server.getRegistryManager());
|
||||
SaveProperties saveProperties = session.readLevelProperties(registryOps, dataPackSettings2);
|
||||
if (saveProperties == null) return LazyValue.NULL;
|
||||
//session.backupLevelDataFile(server.getRegistryManager(), saveProperties); // no need
|
||||
|
||||
// MinecraftServer.createWorlds
|
||||
// save properties should now contain dimension settings
|
||||
GeneratorOptions generatorOptions = saveProperties.getGeneratorOptions();
|
||||
boolean bl = generatorOptions.isDebugWorld();
|
||||
long l = generatorOptions.getSeed();
|
||||
long m = BiomeAccess.hashSeed(l);
|
||||
Map<RegistryKey<World>, ServerWorld> existing_worlds = ((MinecraftServerInterface)server).getCMWorlds();
|
||||
List<Value> addeds = new ArrayList<>();
|
||||
for (Map.Entry<RegistryKey<DimensionOptions>, DimensionOptions> entry : generatorOptions.getDimensions().getEntries()) {
|
||||
RegistryKey<DimensionOptions> registryKey = entry.getKey();
|
||||
if (!existing_worlds.containsKey(registryKey))
|
||||
{
|
||||
addeds.add(ValueConversions.of(registryKey.getValue()));
|
||||
RegistryKey<World> registryKey2 = RegistryKey.of(Registry.DIMENSION, registryKey.getValue());
|
||||
DimensionType dimensionType3 = entry.getValue().getDimensionType();
|
||||
ChunkGenerator chunkGenerator3 = entry.getValue().getChunkGenerator();
|
||||
UnmodifiableLevelProperties unmodifiableLevelProperties = new UnmodifiableLevelProperties(saveProperties, saveProperties.getMainWorldProperties());
|
||||
ServerWorld serverWorld2 = new ServerWorld(server, Util.getMainWorkerExecutor(), session, unmodifiableLevelProperties, registryKey2, dimensionType3, WorldTools.NOOP_LISTENER, chunkGenerator3, bl, m, ImmutableList.of(), false);
|
||||
server.getOverworld().getWorldBorder().addListener(new WorldBorderListener.WorldBorderSyncer(serverWorld2.getWorldBorder()));
|
||||
existing_worlds.put(registryKey2, serverWorld2);
|
||||
}
|
||||
}
|
||||
Value ret = ListValue.wrap(addeds);
|
||||
return (_c, _t) -> ret;
|
||||
});
|
||||
}
|
||||
|
||||
private static void zipValueToJson(Path path, Value output) throws IOException
|
||||
{
|
||||
JsonElement element = output.toJson();
|
||||
if (element == null)
|
||||
throw new InternalExpressionException("Cannot interpret "+output.getPrettyString()+" as a json object");
|
||||
String string = GSON.toJson(element);
|
||||
Files.createDirectories(path.getParent());
|
||||
BufferedWriter bufferedWriter = Files.newBufferedWriter(path);
|
||||
Throwable incident = null;
|
||||
try
|
||||
{
|
||||
bufferedWriter.write(string);
|
||||
}
|
||||
catch (Throwable shitHappened)
|
||||
{
|
||||
incident = shitHappened;
|
||||
throw shitHappened;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (incident != null) {
|
||||
try {
|
||||
bufferedWriter.close();
|
||||
} catch (Throwable otherShitHappened) {
|
||||
incident.addSuppressed(otherShitHappened);
|
||||
}
|
||||
} else {
|
||||
bufferedWriter.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void walkTheDPMap(MapValue node, Path path) throws IOException
|
||||
{
|
||||
Map<Value,Value> items = node.getMap();
|
||||
for (Map.Entry<Value, Value> entry : items.entrySet())
|
||||
{
|
||||
Value val = entry.getValue();
|
||||
String strkey = entry.getKey().getString();
|
||||
Path child = path.resolve(strkey);
|
||||
if (strkey.endsWith(".json"))
|
||||
{
|
||||
zipValueToJson(child, val);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(val instanceof MapValue)) throw new InternalExpressionException("Value of "+strkey+" should be a map");
|
||||
Files.createDirectory(child);
|
||||
walkTheDPMap((MapValue) val, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static <T> Stat<T> getStat(StatType<T> type, Identifier id)
|
||||
{
|
||||
T key = type.getRegistry().get(id);
|
||||
|
||||
@@ -93,7 +93,7 @@ public class Inventories {
|
||||
Item item = NBTSerializableValue.parseItem(lv.get(0).evalValue(c).getString()).getItem();
|
||||
if (lv.size() == 1)
|
||||
{
|
||||
Value ret = ListValue.wrap(tagManager.getItems().getTagsFor(item).stream().map(ValueConversions::of).collect(Collectors.toList()));
|
||||
Value ret = ListValue.wrap(tagManager.getItems().getTags().entrySet().stream().filter(e -> e.getValue().contains(item)).map(e -> ValueConversions.of(e.getKey())).collect(Collectors.toList()));
|
||||
return (_c, _t) -> ret;
|
||||
}
|
||||
String tag = lv.get(1).evalValue(c).getString();
|
||||
|
||||
@@ -1127,7 +1127,8 @@ public class WorldAccess {
|
||||
BlockArgument blockLocator = BlockArgument.findIn(cc, lv, 0, true);
|
||||
if (blockLocator.offset == lv.size())
|
||||
{
|
||||
Value ret = ListValue.wrap(tagManager.getBlocks().getTagsFor(blockLocator.block.getBlockState().getBlock()).stream().map(ValueConversions::of).collect(Collectors.toList()));
|
||||
Block target = blockLocator.block.getBlockState().getBlock();
|
||||
Value ret = ListValue.wrap(tagManager.getBlocks().getTags().entrySet().stream().filter(e -> e.getValue().contains(target)).map(e -> ValueConversions.of(e.getKey())).collect(Collectors.toList()));
|
||||
return (_c, _t) -> ret;
|
||||
}
|
||||
String tag = lv.get(blockLocator.offset).evalValue(c).getString();
|
||||
@@ -1381,6 +1382,7 @@ public class WorldAccess {
|
||||
{
|
||||
if (lv.size() == 0) throw new InternalExpressionException("'custom_dimension' requires at least one argument");
|
||||
CarpetContext cc = (CarpetContext)c;
|
||||
cc.host.issueDeprecation("custom_dimension()");
|
||||
String worldKey = lv.get(0).evalValue(c).getString();
|
||||
|
||||
Long seed = null;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class Sys {
|
||||
return (cc, tt) -> Value.FALSE;
|
||||
}
|
||||
}
|
||||
Value retval = new NumericValue(v.getBoolean());
|
||||
Value retval = v.getBoolean()?Value.TRUE:Value.FALSE;
|
||||
return (cc, tt) -> retval;
|
||||
});
|
||||
|
||||
@@ -56,7 +56,9 @@ public class Sys {
|
||||
{
|
||||
if (v instanceof NumericValue)
|
||||
{
|
||||
return v;
|
||||
NumericValue num = (NumericValue)v;
|
||||
if (num.isInteger()) return new NumericValue(num.getLong());
|
||||
return new NumericValue(num.getDouble());
|
||||
}
|
||||
try
|
||||
{
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package carpet.script.utils;
|
||||
|
||||
import carpet.script.exception.InternalExpressionException;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class InputValidator {
|
||||
public static String validateSimpleString(String input, boolean strict)
|
||||
{
|
||||
String simplified = input.toLowerCase(Locale.ROOT).replaceAll("[^A-Za-z0-9+_]", "");
|
||||
if (simplified.isEmpty() || (strict && !simplified.equals(input)))
|
||||
throw new InternalExpressionException("simple name can only contain numbers, letter and _");
|
||||
return simplified;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import carpet.script.value.MapValue;
|
||||
import carpet.script.value.NumericValue;
|
||||
import carpet.script.value.StringValue;
|
||||
import carpet.script.value.Value;
|
||||
import carpet.script.value.ValueConversions;
|
||||
import carpet.settings.ParsedRule;
|
||||
import carpet.settings.SettingsManager;
|
||||
import com.sun.management.OperatingSystemMXBean;
|
||||
@@ -49,6 +50,7 @@ public class SystemInfo {
|
||||
String tlf = serverPath.getName(nodeCount-2).toString();
|
||||
return StringValue.of(tlf);
|
||||
});
|
||||
put("world_dimensions", c -> ListValue.wrap(c.s.getMinecraftServer().getWorldRegistryKeys().stream().map(k -> ValueConversions.of(k.getValue())).collect(Collectors.toList())));
|
||||
put("game_difficulty", c -> StringValue.of(c.s.getMinecraftServer().getSaveProperties().getDifficulty().getName()));
|
||||
put("game_hardcore", c -> new NumericValue(c.s.getMinecraftServer().getSaveProperties().isHardcore()));
|
||||
put("game_storage_format", c -> StringValue.of(c.s.getMinecraftServer().getSaveProperties().getFormatName(c.s.getMinecraftServer().getSaveProperties().getVersion())));
|
||||
|
||||
@@ -157,5 +157,5 @@ public class WorldTools
|
||||
@Override public void stop() { }
|
||||
}
|
||||
|
||||
private static final WorldGenerationProgressListener NOOP_LISTENER = new NoopWorldGenerationProgressListener();
|
||||
public static final WorldGenerationProgressListener NOOP_LISTENER = new NoopWorldGenerationProgressListener();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package carpet.script.value;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import net.minecraft.nbt.ByteTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
|
||||
public class BooleanValue extends NumericValue
|
||||
{
|
||||
public static final BooleanValue FALSE = new BooleanValue(false);
|
||||
public static final BooleanValue TRUE = new BooleanValue(true);
|
||||
|
||||
boolean boolValue;
|
||||
private BooleanValue(boolean boolval) {
|
||||
super(boolval);
|
||||
boolValue = boolval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString() {
|
||||
return boolValue?"true":"false";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrettyString() {
|
||||
return getString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeString() {
|
||||
return "bool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value clone() {
|
||||
return new BooleanValue(boolValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Boolean.hashCode(boolValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tag toTag(boolean force) {
|
||||
return ByteTag.of(boolValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement toJson() {
|
||||
return new JsonPrimitive(boolValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInteger() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -416,7 +416,7 @@ public class EntityValue extends Value
|
||||
put("motion_z", (e, a) -> new NumericValue(e.getVelocity().z));
|
||||
put("on_ground", (e, a) -> new NumericValue(e.isOnGround()));
|
||||
put("name", (e, a) -> new StringValue(e.getName().getString()));
|
||||
put("display_name", (e, a) -> new StringValue(e.getDisplayName().getString()));
|
||||
put("display_name", (e, a) -> new FormattedTextValue(e.getDisplayName()));
|
||||
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()))));
|
||||
@@ -429,7 +429,7 @@ public class EntityValue extends Value
|
||||
put("tags", (e, a) -> ListValue.wrap(e.getScoreboardTags().stream().map(StringValue::new).collect(Collectors.toList())));
|
||||
|
||||
put("scoreboard_tags", (e, a) -> ListValue.wrap(e.getScoreboardTags().stream().map(StringValue::new).collect(Collectors.toList())));
|
||||
put("entity_tags", (e, a) -> ListValue.wrap(e.getServer().getTagManager().getEntityTypes().getTagsFor(e.getType()).stream().map(ValueConversions::of).collect(Collectors.toList())));
|
||||
put("entity_tags", (e, a) -> ListValue.wrap(e.getServer().getTagManager().getEntityTypes().getTags().entrySet().stream().filter(entry -> entry.getValue().contains(e.getType())).map(entry -> ValueConversions.of(entry.getKey())).collect(Collectors.toList())));
|
||||
// deprecated
|
||||
put("has_tag", (e, a) -> new NumericValue(e.getScoreboardTags().contains(a.getString())));
|
||||
|
||||
@@ -462,7 +462,7 @@ public class EntityValue extends Value
|
||||
return ListValue.of(new NumericValue(look.x),new NumericValue(look.y),new NumericValue(look.z));
|
||||
});
|
||||
put("is_burning", (e, a) -> new NumericValue(e.isOnFire()));
|
||||
//put("fire", (e, a) -> new NumericValue(e.getFire())); needs mixing
|
||||
put("fire", (e, a) -> new NumericValue(e.getFireTicks()));
|
||||
put("silent", (e, a)-> new NumericValue(e.isSilent()));
|
||||
put("gravity", (e, a) -> new NumericValue(!e.hasNoGravity()));
|
||||
put("immune_to_fire", (e, a) -> new NumericValue(e.isFireImmune()));
|
||||
|
||||
@@ -341,7 +341,7 @@ public class ListValue extends AbstractListValue implements ContainerValueInterf
|
||||
int from = normalizeIndex(fromDesc, size);
|
||||
if (toDesc == null)
|
||||
return new ListValue((Collection<? extends Value>) getItems().subList(from, size));
|
||||
int to = normalizeIndex(toDesc, size);
|
||||
int to = normalizeIndex(toDesc, size+1);
|
||||
if (from > to)
|
||||
return ListValue.of();
|
||||
return new ListValue((Collection<? extends Value>) getItems().subList(from, to));
|
||||
|
||||
@@ -7,6 +7,7 @@ import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -16,7 +17,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class MapValue extends AbstractListValue implements ContainerValueInterface
|
||||
{
|
||||
private Map<Value, Value> map;
|
||||
private final Map<Value, Value> map;
|
||||
|
||||
private MapValue()
|
||||
{
|
||||
@@ -268,7 +269,9 @@ public class MapValue extends AbstractListValue implements ContainerValueInterfa
|
||||
public JsonElement toJson()
|
||||
{
|
||||
JsonObject jsonMap = new JsonObject();
|
||||
map.forEach((k, v) -> jsonMap.add(k.getString(), v.toJson()));
|
||||
List<Value> keys = new ArrayList<>(map.keySet());
|
||||
Collections.sort(keys);
|
||||
keys.forEach( k -> jsonMap.add(k.getString(), map.get(k).toJson()));
|
||||
return jsonMap;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import net.minecraft.nbt.Tag;
|
||||
|
||||
public class NullValue extends NumericValue // TODO check nonsingleton code
|
||||
{
|
||||
public static final NullValue NULL = new NullValue();
|
||||
@Override
|
||||
public String getString()
|
||||
{
|
||||
@@ -32,7 +33,7 @@ public class NullValue extends NumericValue // TODO check nonsingleton code
|
||||
{
|
||||
return new NullValue();
|
||||
}
|
||||
public NullValue() {super(0.0D);}
|
||||
private NullValue() {super(false);}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o)
|
||||
|
||||
@@ -15,12 +15,12 @@ import java.util.stream.Collectors;
|
||||
|
||||
public abstract class Value implements Comparable<Value>, Cloneable
|
||||
{
|
||||
public static NumericValue FALSE = new NumericValue(0);
|
||||
public static NumericValue TRUE = new NumericValue(1);
|
||||
public static NumericValue ZERO = FALSE;
|
||||
public static NumericValue ONE = TRUE;
|
||||
public static NumericValue FALSE = BooleanValue.FALSE;
|
||||
public static NumericValue TRUE = BooleanValue.TRUE;
|
||||
public static NumericValue ZERO = new NumericValue(0);
|
||||
public static NumericValue ONE = new NumericValue(1);
|
||||
|
||||
public static NullValue NULL = new NullValue();
|
||||
public static NullValue NULL = NullValue.NULL;
|
||||
|
||||
public String boundVariable;
|
||||
|
||||
@@ -186,7 +186,7 @@ public abstract class Value implements Comparable<Value>, Cloneable
|
||||
int size = value.length();
|
||||
int from = ListValue.normalizeIndex(fromDesc, size);
|
||||
if (toDesc == null) return new StringValue(value.substring(from));
|
||||
int to = ListValue.normalizeIndex(toDesc, size);
|
||||
int to = ListValue.normalizeIndex(toDesc, size+1);
|
||||
if (from > to) return StringValue.EMPTY;
|
||||
return new StringValue(value.substring(from, to));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__command() ->
|
||||
_command() ->
|
||||
(
|
||||
print('camera: scarpet app.');
|
||||
print('-------------------');
|
||||
@@ -11,7 +11,7 @@ __command() ->
|
||||
print(' "/camera place_player - Move player to the selected point');
|
||||
print(' "/camera move" - Move the selected point to players location');
|
||||
print(' "/camera duration <X>" - Set new selected path duration');
|
||||
print(' "/camera split_point>" - Split selected path in half.');
|
||||
print(' "/camera split_point" - Split selected path in half.');
|
||||
print(' "/camera delete_point" - Remove current key point');
|
||||
print(' "/camera trim_path" - Remove all key points from selected up');
|
||||
print('');
|
||||
@@ -57,6 +57,47 @@ __command() ->
|
||||
print('');
|
||||
null
|
||||
);
|
||||
__config() ->{
|
||||
'commands'->{
|
||||
''->'_command',
|
||||
'<command>'->'_call',
|
||||
'add <seconds>'->'add',
|
||||
'prepend <seconds>'->'prepend',
|
||||
'duration <seconds>' -> 'duration',
|
||||
'save_as <name>'->'save_as',
|
||||
'load <name>'->'load',
|
||||
'interpolation <interpolation>'->['__interpolation',true],
|
||||
'interpolation gauss'->['__interpolation','gauss',true],
|
||||
'interpolation gauss <float>'->_(float)->(__interpolation('gauss_'+str(float),true)),
|
||||
'repeat <seconds> <last_delay>'->'repeat',
|
||||
'stretch <factor>'->'stretch'
|
||||
},
|
||||
'arguments'->{
|
||||
'seconds'->{'type'->'int','suggest'->[]},
|
||||
'last_delay'->{'type'->'int','suggest'->[]},
|
||||
'name'->{'type'->'string','suggest'->[]},
|
||||
'interpolation'->{'type'->'term','options'->['linear','catmull_rom']},
|
||||
'factor'->{'type'->'int','min'->25,'max'->400},
|
||||
'command'->{'type'->'term','options'->[
|
||||
'start',
|
||||
'clear',
|
||||
'select',
|
||||
'place_player',
|
||||
'move',
|
||||
'split_point',
|
||||
'delete_point',
|
||||
'trim_path',
|
||||
'transpose',
|
||||
'play',
|
||||
'show',
|
||||
'hide',
|
||||
'prefer_smooth_play',
|
||||
'prefer_synced_play'
|
||||
]}
|
||||
}
|
||||
};
|
||||
|
||||
_call(command)->call(command);
|
||||
|
||||
global_points = null;
|
||||
global_dimension = null;
|
||||
@@ -150,7 +191,6 @@ __assert_point_selected(validator) ->
|
||||
);
|
||||
|
||||
//select a custom interpolation method
|
||||
interpolation(method) -> __interpolation(method, true);
|
||||
__interpolation(method, verbose) ->
|
||||
(
|
||||
__prepare_path_if_needed() -> __prepare_path_if_needed_generic();
|
||||
@@ -159,7 +199,7 @@ __interpolation(method, verbose) ->
|
||||
// or optionally __prepare_path_if_needed, if path is inefficient to compute point by point
|
||||
global_interpolator = if (
|
||||
method == 'linear', '__interpolator_linear',
|
||||
method == 'cr', '__interpolator_cr',
|
||||
method == 'catmull_rom', '__interpolator_cr',
|
||||
method == 'gauss', _(s, p) -> __interpolator_gauB(s, p, 0),
|
||||
method ~ '^gauss_',
|
||||
(
|
||||
@@ -167,13 +207,12 @@ __interpolation(method, verbose) ->
|
||||
type = replace(type,'_','.');
|
||||
variance = round(60*number(type));
|
||||
_(s, p, outer(variance)) -> __interpolator_gauB(s, p, variance);
|
||||
),
|
||||
exit('Choose one of the following methods: linear, gauss, gauss_<deviation>, cr')
|
||||
)
|
||||
);
|
||||
__update();
|
||||
if(verbose, 'Interpolation changed to '+method, '');
|
||||
);
|
||||
__interpolation('cr', false);
|
||||
__interpolation('catmull_rom', false);
|
||||
|
||||
// adds a point to the end of the path with delay in seconds
|
||||
add(delay) ->
|
||||
@@ -216,9 +255,6 @@ repeat(times, last_section_delay) ->
|
||||
stretch(percentage) ->
|
||||
(
|
||||
if (err = __is_not_valid_for_motion(), exit(err));
|
||||
if (percentage < 25 || percentage > 400,
|
||||
exit('path speed can only be speed, or slowed down 4 times. Re-call command for larger changes')
|
||||
);
|
||||
ratio = percentage/100;
|
||||
previous_path_length = global_points:(-1):1;
|
||||
for(global_points, _:1 = _:1*ratio );
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"ServerPlayNetworkHandler_interactionUpdatesMixin",
|
||||
|
||||
"ServerMetadata_motdMixin",
|
||||
"MinecraftServer_pingPlayerSampleLimit",
|
||||
|
||||
"MinecraftServer_coreMixin",
|
||||
|
||||
@@ -175,6 +176,7 @@
|
||||
"PointOfInterest_scarpetMixin",
|
||||
"ServerWorld_scarpetMixin",
|
||||
"HungerManagerMixin_scarpetEventsMixin",
|
||||
"ReloadCommand_reloadAppsMixin",
|
||||
|
||||
"RecipeManager_scarpetMixin",
|
||||
"Ingredient_scarpetMixin",
|
||||
|
||||
Reference in new issue
Block a user