diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b57fd4b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sc linguist-language=Python +*.scl linguist-language=Python diff --git a/.github/workflows/build-on-pr.yml b/.github/workflows/build-on-pr.yml new file mode 100644 index 0000000..ad8e998 --- /dev/null +++ b/.github/workflows/build-on-pr.yml @@ -0,0 +1,28 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Pull Request Builds + +on: [pull_request] + +jobs: + Build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 21 + cache: 'gradle' + - name: Grant execute permission to gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - uses: actions/upload-artifact@v3 + with: + name: "Compiled artifacts for Pull Request #${{github.event.number}}" + path: build/libs diff --git a/.github/workflows/devbuild.yml b/.github/workflows/devbuild.yml new file mode 100644 index 0000000..15dbeaf --- /dev/null +++ b/.github/workflows/devbuild.yml @@ -0,0 +1,28 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Development Builds + +on: [push] + +jobs: + Build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 21 + cache: 'gradle' + - name: Grant execute permission to gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - uses: actions/upload-artifact@v3 + with: + name: Compiled artifacts for ${{ github.sha }} + path: build/libs diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..6872048 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,192 @@ +name: Publish Release + +on: + release: + types: [published] +jobs: + Get-Properties: + runs-on: ubuntu-latest + outputs: + release-type: ${{ steps.type.outputs.release-type }} + carpet-version: ${{ steps.properties.outputs.mod_version }} + minecraft-version: ${{ steps.properties.outputs.minecraft_version }} + curse-versions: ${{ steps.properties.outputs.release-curse-versions }} + matrix-exclude-branch: ${{ steps.processmatrix.outputs.matrix-to-exclude }} + extra-branch-name: ${{ steps.properties.outputs.release-extra-branch-name }} + extra-branch-curse-version: ${{ steps.properties.outputs.release-extra-curse-version }} + steps: + - name: Checkout the sources + uses: actions/checkout@v3 + - name: Determine release type + id: type + run: | + if ${{ github.event.release.prerelease }}; then + echo "release-type=beta" >> $GITHUB_OUTPUT + else + echo "release-type=release" >> $GITHUB_OUTPUT + fi + - name: Read relevant fields from gradle.properties + id: properties + run: | # From christian-draeger/read-properties, using the action makes it extremely messy until christian-draeger/read-properties#2 + path='./gradle.properties' + for property in mod_version minecraft_version release-curse-versions release-extra-branch release-extra-branch-name release-extra-curse-version + do + result=$(sed -n "/^[[:space:]]*$property[[:space:]]*=[[:space:]]*/s/^[[:space:]]*$property[[:space:]]*=[[:space:]]*//p" "$path") + echo "$property: $result" + echo "$property=$result" >> $GITHUB_OUTPUT + done + - name: Process property for matrix + id: processmatrix + run: | + if ! ${{ steps.properties.outputs.release-extra-branch }}; then + echo "matrix-to-exclude=Snapshots" >> $GITHUB_OUTPUT + fi + - uses: actions/github-script@v6 + env: + READ_VERSION: ${{ steps.properties.outputs.mod_version }} + with: + script: | + const { READ_VERSION } = process.env; + console.log('Read version is: ' + READ_VERSION); + let releases = (await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo + })).data; + console.log('Previous release was: ' + releases[1].name); + for (let release of releases.slice(1)) { + if (release.name.includes(READ_VERSION)) + core.setFailed('Version number is the same as a previous release!') + } + Build-And-Publish: + runs-on: ubuntu-latest + needs: [Get-Properties] + strategy: + matrix: + branch: [Release, Snapshots] + exclude: + - branch: ${{ needs.Get-Properties.outputs.matrix-exclude-branch }} + steps: + - name: Get info from branch to run + id: getbranchinfo + run: | + if ${{ matrix.branch == 'Snapshots'}}; then + echo "branchname=${{ needs.Get-Properties.outputs.extra-branch-name }}" >> $GITHUB_OUTPUT + echo "version=${{ needs.Get-Properties.outputs.extra-branch-curse-version }}" >> $GITHUB_OUTPUT + echo "curse-versions=${{ needs.Get-Properties.outputs.extra-branch-curse-version }}" >> $GITHUB_OUTPUT + else + echo "version=${{ needs.Get-Properties.outputs.minecraft-version }}" >> $GITHUB_OUTPUT + echo "curse-versions=${{ needs.Get-Properties.outputs.curse-versions }}" >> $GITHUB_OUTPUT + fi + - name: Checkout the sources + uses: actions/checkout@v3 + with: + ref: ${{ steps.getbranchinfo.outputs.branchname }} + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 21 + cache: 'gradle' + - name: Grant execute permission to gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - name: Find correct JAR + id: findjar + run: | + output="$(find build/libs/ ! -name "*-dev.jar" ! -name "*-sources.jar" -type f -printf "%f\n")" + echo "jarname=$output" >> $GITHUB_OUTPUT + - name: Save build artifacts in the action + uses: actions/upload-artifact@v3 + with: + name: Artifacts for ${{ matrix.branch }} + path: build/libs + - name: Upload to the Github release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: build/libs/${{ steps.findjar.outputs.jarname }} + asset_name: ${{ steps.findjar.outputs.jarname }} + asset_content_type: application/java-archive + - name: Upload to Curseforge + uses: itsmeow/curseforge-upload@v3 + with: + token: ${{ secrets.CF_API_TOKEN }} + project_id: 349239 + game_endpoint: minecraft + file_path: build/libs/${{ steps.findjar.outputs.jarname }} + changelog_type: markdown + changelog: ${{ github.event.release.body }} + display_name: Carpet v${{ needs.Get-Properties.outputs.carpet-version }} for ${{ steps.getbranchinfo.outputs.version }} + game_versions: 7499,4458,${{ steps.getbranchinfo.outputs.curse-versions }} #Fabric,Java 8,[version (s) for the branch] + release_type: ${{ needs.Get-Properties.outputs.release-type }} + - name: Ask Gradle to publish + run: ./gradlew publish + - name: Save publish folder in action's artifacts # Remove when automated + uses: actions/upload-artifact@v3 + with: + name: Maven publishing artifacts for ${{ matrix.branch }} + path: publish/carpet/fabric-carpet/ + Publish-To-Discord: + runs-on: ubuntu-latest + needs: [Build-And-Publish] + steps: + - name: Publish to discord + uses: Crec0/announce-n-crosspost@v1 + with: + bot-token: ${{ secrets.DISCORD_BOT_TOKEN }} + channel: '897934715200339999' + content: | + **${{ github.event.release.name }}** has been released! + + ${{ github.event.release.body }} + ​ + Get it on Github Releases: <${{ github.event.release.html_url }}> + Or on CurseForge + Merge-Scarpet-Docs: + runs-on: ubuntu-latest + steps: + - name: Checkout the sources + uses: actions/checkout@v3 + with: + ref: master + - name: Merge docs + run: | + ./mergedoc.sh + - name: Commit merged docs + continue-on-error: true + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git commit -am "Merge docs for '${{ github.event.release.name }}'" || exit 0 + git push + Update-Rules-Wiki: + runs-on: ubuntu-latest + steps: + - name: Checkout Carpet sources + uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 21 + cache: 'gradle' + - name: Checkout wiki + uses: actions/checkout@v3 + with: + repository: ${{github.repository}}.wiki + path: wiki + - name: Run rule printer into the wiki page + run: | + chmod +x gradlew + ./gradlew runServer --args="-- -carpetDumpRules -dumpPath ../wiki/Current-Available-Settings.md" + - name: Commit updated wiki page + continue-on-error: true + run: | + cd wiki + git config --global user.name 'github-actions-bot' # Releases don't have valid commiter info :( + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + git commit -am "Update wiki for '${{ github.event.release.name }}'" || exit 0 + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db0d4ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# gradle + +.gradle/ +build/ +out/ +lout/ +classes/ +publish/ + +# git + +.shelf/ + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# eclipse +*.launch + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project +*.code-workspace + +# fabric + +run/ +logs/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..467d914 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 gnembon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4a03ab --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ + + +# Fabric Carpet + +[![Development Builds](https://github.com/gnembon/fabric-carpet/actions/workflows/devbuild.yml/badge.svg)](https://github.com/gnembon/fabric-carpet/actions/workflows/devbuild.yml) +[![CurseForge downloads](http://cf.way2muchnoise.eu/full_349239_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/carpet) +[![Modrinth downloads](https://img.shields.io/modrinth/dt/carpet?label=Modrinth%20downloads&logo=modrinth)](https://modrinth.com/mod/carpet) +[![GitHub downloads](https://img.shields.io/github/downloads/gnembon/fabric-carpet/total?label=Github%20downloads&logo=github)](https://github.com/gnembon/fabric-carpet/releases) +[![GitHub contributors](https://img.shields.io/github/contributors/gnembon/fabric-carpet?label=Contributors&logo=github)](https://github.com/gnembon/fabric-carpet/graphs/contributors) +[![Discord](https://badgen.net/discord/online-members/gn99m4QRY4?icon=discord&label=Discord&list=what)](https://discord.gg/gn99m4QRY4) + +Cause all carpets are made of fabric? +This is the [Fabric](https://fabricmc.net/) version of Carpet Mod, for Minecraft 1.14.X, 1.15.X, 1.16.X and above. + +Carpet Mod is a mod for vanilla Minecraft that allows you to take full control of what matters from a technical perspective of the game. + +* Test your farms over several hours in only a few minutes using [`/tick warp`](https://github.com/gnembon/fabric-carpet/wiki/Commands#usage-tick-warp-ticks-cmd), as fast as your computer can +* ...and then see a detailed breakdown of the items they produce using [`hopperCounters`](https://github.com/gnembon/fabric-carpet/wiki/Current-Available-Settings#hoppercounters) +* See the server mobcap, TPS, etc. update live with [`/log`](https://github.com/gnembon/fabric-carpet/wiki/Commands#log) +* Let pistons push block entities (ie. chests) with [`movableBlockEntities`](https://github.com/gnembon/fabric-carpet/wiki/Current-Available-Settings#movableblockentities) +* [Fix](https://github.com/gnembon/fabric-carpet/wiki/Current-Available-Settings#leadfix) [many](https://github.com/gnembon/fabric-carpet/wiki/Current-Available-Settings#portalsuffocationfix) [things](https://github.com/gnembon/fabric-carpet/wiki/Current-Available-Settings#unloadedentityfix) + +# Carpet mod ecosystem + +For core carpet functionality, this is the right place. Check available downloads on the [release page](https://github.com/gnembon/fabric-carpet/releases) + + + +## carpet-extra + +Check [carpet-extra](https://github.com/gnembon/carpet-extra/) add-on mod for more whacky and crazy features, including autocrafting, block-placing dispensers, and even chicken-shearing! + +## scarpet app store + +If you want to browse or contribute to the scarpet app store check available apps, go [here](https://github.com/gnembon/scarpet), its free! + +## quick-carpet + +Have problems with the recent snapshot or someone is slacking with releasing the base carpet mod, but you need some basic carpet functionality, check the minimal build of [quick-carpet](https://github.com/gnembon/quick-carpet/releases). It hopefully works just fine. You will not be able to add any extensions or use scarpet apps on this one though. + +## Other community-based extensions to carpet + +Here is a [list of other carpet extensions](https://github.com/gnembon/fabric-carpet/wiki/List-of-Carpet-extensions) created by the community. + +Everybody can create their own carpet features or extend scarpet language with some new API, by creating a carpet extension using this [carpet extension mod template](https://github.com/gnembon/fabric-carpet-extension-example-mod). + +# How? Hwat? + +Follow instructions for all other fabric mods in https://fabricmc.net/use/ and dump [carpet...jar](https://github.com/gnembon/fabric-carpet/releases) in `mods` folder along with other compatible mods. + +See the [mod showcase](https://www.youtube.com/watch?v=Lt-ooRGpLz4) on youtube for an explanation of every setting and command, and check the List of all currently available settings on the wiki for an updated list of every setting. + +# Carpet Mod Settings +See [List of all currently available settings][settings] on the wiki + +[settings]: https://github.com/gnembon/fabric-carpet/wiki/Current-Available-Settings + +For previous Minecraft versions: 1.13 check [gnembon/carpetmod](https://github.com/gnembon/carpetmod) and for 1.12 check [gnembon/carpetmod112](https://github.com/gnembon/carpetmod112). diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..678f16a --- /dev/null +++ b/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'fabric-loom' version '1.6-SNAPSHOT' + id 'io.github.juuxel.loom-quiltflower' version '1.7.3' + id 'maven-publish' +} + +sourceCompatibility = JavaVersion.VERSION_21 +targetCompatibility = JavaVersion.VERSION_21 + +archivesBaseName = project.archives_base_name +version = project.minecraft_version+'-'+project.mod_version+'+v'+new Date().format('yyMMdd') +group = project.maven_group + +loom { + accessWidenerPath = file("src/main/resources/carpet.accesswidener") + runtimeOnlyLog4j = true +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + //modImplementation "net.fabricmc:fabric:${project.fabric_version}" + //modCompileOnly fabricApi.module("fabric-rendering-v1", project.fabric_version) + + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. + compileOnly "com.google.code.findbugs:jsr305:${project.jsr305_version}" +} + +processResources { + inputs.property "version", project.version+'+v'+new Date().format('yyMMdd') + + filesMatching("fabric.mod.json") { + expand "version": project.mod_version+'+v'+new Date().format('yyMMdd') + } +} + + +tasks.withType(JavaCompile).configureEach { + // ensure that the encoding is set to UTF-8, no matter what the system default is + // this fixes some edge cases with special characters not displaying correctly + // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html + // If Javadoc is generated, this must be specified in that task too. + it.options.encoding = "UTF-8" + + // Minecraft 1.20.5 upwards uses Java 21. + it.options.release = 21 +} + +java { + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}"} + } +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } + + // Select the repositories you want to publish to + // To publish to maven local, no extra repositories are necessary. Just use the task `publishToMavenLocal`. + repositories { + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + maven { + url "$projectDir/publish" + } + } +} diff --git a/docs/scarpet/Documentation.md b/docs/scarpet/Documentation.md new file mode 100644 index 0000000..d56ad07 --- /dev/null +++ b/docs/scarpet/Documentation.md @@ -0,0 +1,42 @@ +# Scarpet Language Documentation + +## Language Specification + +[Overview](/docs/scarpet/language/Overview.md) + +[Variables and Constants](/docs/scarpet/language/VariablesAndConstants.md) + +[Operators](/docs/scarpet/language/Operators.md) + +[Mathematical Functions](/docs/scarpet/language/Math.md) + +[System Functions](/docs/scarpet/language/SystemFunctions.md) + +[Loops and Higher Order Functions](/docs/scarpet/language/LoopsAndHigherOrderFunctions.md) + +[User Defined Functions and Control Flow](/docs/scarpet/language/FunctionsAndControlFlow.md) + +[Container Types](/docs/scarpet/language/Containers.md) + +## Minecraft API + +[Overview](/docs/scarpet/api/Overview.md) + +[Blocks and World Access](/docs/scarpet/api/BlocksAndWorldAccess.md) + +[Block Iterations](/docs/scarpet/api/BlockIterations.md) + +[Entities](/docs/scarpet/api/Entities.md) + +[Inventories](/docs/scarpet/api/Inventories.md) + +[Events](/docs/scarpet/api/Events.md) + +[Scoreboard manipulation](/docs/scarpet/api/Scoreboard.md) + +[Auxiliary Functions](/docs/scarpet/api/Auxiliary.md) + +[`/script` Command](/docs/scarpet/api/ScriptCommand.md) + +## [One Page](/docs/scarpet/Full.md) + diff --git a/docs/scarpet/Full.md b/docs/scarpet/Full.md new file mode 100644 index 0000000..3c70c64 --- /dev/null +++ b/docs/scarpet/Full.md @@ -0,0 +1,6330 @@ +# Fundamental components of `scarpet` programming language. + +Scarpet (a.k.a. Carpet Script, or Script for Carpet) is a programming language +designed to provide the ability to write custom programs to run within Minecraft +and interact with the world. + +This specification is divided into two sections: this one is agnostic to any +Minecraft related features and could function on its own, and CarpetExpression +for Minecraft specific routines and world manipulation functions. + +# Synopsis + +
+script run print('Hello World!')
+
+ +or an OVERLY complex example: + +
+/script run
+    block_check(x1, y1, z1, x2, y2, z2, block_to_check) ->
+    (
+        [minx, maxx] = sort([x1, x2]);
+        [miny, maxy] = sort([y1, y2]);
+        [minz, maxz] = sort([z1, z2]);
+        'Need to compute the size of the area of course';
+        'Cause this language doesn\'t support comments in the command mode';
+        xsize = maxx - minx + 1;
+        ysize = maxy - miny + 1;
+        zsize = maxz - minz + 1;
+        total_count = 0;
+        loop(xsize,
+            xx = minx + _ ;
+            loop(ysize,
+                yy = miny + _ ;
+                loop(zsize,
+                    zz = minz + _ ;
+                    if ( block(xx,yy,zz) == block_to_check,
+                        total_count += ceil(rand(1))
+                    )
+                )
+            )
+        );
+        total_count
+    );
+    check_area_around_closest_player_for_block(block_to_check) ->
+    (
+        closest_player = player();
+        [posx, posy, posz] = query(closest_player, 'pos');
+        total_count = block_check( posx-8,1,posz-8, posx+8,17,posz+8, block_to_check);
+        print('There is '+total_count+' of '+block_to_check+' around you')
+    )
+/script invoke check_area_around_closest_player_for_block 'diamond_ore'
+
+ +or simply + +
+/script run print('There is '+for(rect(x,9,z,8,8,8), _ == 'diamond_ore')+' diamond ore around you')
+
+ +It definitely pays to check what higher level `scarpet` functions have to offer. + +# Programs + +You can think of an program like a mathematical expression, like `"2.4*sin(45)/(2-4)"` or `"sin(y)>0 & max(z, 3)>3"`. +Writing a program, is like writing a `2+3`, just a bit longer. + +## Basic language components + +Programs consist of constants, like `2`, `3.14`, `pi`, or `'foo'`, operators like `+`, `/`, `->`, variables which you +can define, like `foo` or special ones that will be defined for you, like `_x`, or `_` , which are specific to each +built in function, and functions with name, and arguments in the form of `f(a,b,c)`, where `f` is the function name, +and `a, b, c` are the arguments which can be any other expression. And that's all the parts of the language, so all +in all - sounds quite simple. + +## Code flow + +Like any other proper programming language, `scarpet` needs brackets, basically to identify where stuff begins and +where it ends. In the languages that uses much more complicated constructs, like Java, they tend to use all sort of +them, round ones to indicate function calls, curly to indicate section of code, square to access lists, pointy for +generic types etc... I mean - there is no etc, cause they have exhausted all the bracket options... + +`Scarpet` is different, since it runs everything based on functions (although its not per se a functional +language like lisp) only needs the round brackets for everything, and it is up to the programmer to organize +its code so its readable, as adding more brackets does not have any effect on the performance of the programs +as they are compiled before they are executed. Look at the following example usage of `if()` function: + +
+if(x<y+6,set(x,8+y,z,'air');plop(x,top('surface',x,z),z,'birch'),sin(query(player(),'yaw'))>0.5,plop(0,0,0,'boulder'),particle('fire',x,y,z))
+
+ +Would you prefer to read + +
+if(   x<y+6,
+           set(x,8+y,z,'air');
+           plop(x,top('surface',x,z),z,'birch'),
+      sin(query(player(),'yaw'))>0.5,
+           plop(0,0,0,'boulder'),
+      particle('fire',x,y,z)
+)
+
+ +Or rather: + +
+if
+(   x<y+6,
+    (
+        set(x,8+y,z,'air');
+        plop(x,top('surface',x,z),z,'birch')
+    ),
+    // else if
+    sin(query(player(),'yaw'))>0.5,
+    (
+        plop(0,0,0,'boulder')
+    ),
+    // else
+    particle('fire',x,y,z)
+)
+
+ +Whichever style you prefer it doesn't matter. It typically depends on the situation and the complexity of the +subcomponents. No matter how many whitespaces and extra brackets you add - the code will evaluate to exactly the +same expression, and will run exactly the same, so make sure your programs are nice and clean so others don't +have problems with them + +## Functions and scoping + +Users can define functions in the form `fun(args....) -> expression` and they are compiled and saved for further +execution in this, but also subsequent calls of /script command, added to events, etc. Functions can also be + assigned to variables, +passed as arguments, called with `call('fun', args...)` function, but in most cases you would want to +call them directly by +name, in the form of `fun(args...)`. This means that once defined functions are saved with the world for +further use. For variables, there are two types of them, global - which are shared anywhere in the code, +and those are all which name starts with 'global_', and local variables which is everything else and those +are only visible inside each function. This also means that all the parameters in functions are +passed 'by value', not 'by reference'. + +## Outer variables + +Functions can still 'borrow' variables from the outer scope, by adding them to the function signature wrapped +around built-in function `outer`. It adds the specified value to the function call stack so they behave exactly +like capturing lambdas in Java, but unlike java captured variables don't need to be final. Scarpet will just +attach their new values at the time of the function definition, even if they change later. Most value will be +copied, but mutable values, like maps or lists, allow to keep the 'state' with the function, allowing them to +have memory and act like objects so to speak. Check `outer(var)` for details. + +## Code delivery, line indicators + +Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing +your code in apps (files with `.sc` extension that can be placed inside `/scripts` folder in the world files +or as a globally available app in singleplayer in the `.minecraft/config/carpet/scripts` folder and loaded +as a Scarpet app with the command `/script load [app_name]`. Scarpet apps loaded from disk should only +contain code, no need to start with `/script run` prefix. + +The following is the code that could be provided in a `foo.sc` app file located in world `/scripts` folder + +
+run_program() -> (
+  loop( 10,
+    // looping 10 times
+    // comments are allowed in scripts located in world files
+    // since we can tell where that line ends
+    foo = floor(rand(10));
+    check_not_zero(foo);
+    print(_+' - foo: '+foo);
+    print('  reciprocal: '+  _/foo )
+  )
+);
+check_not_zero(foo) -> (
+  if (foo==0, foo = 1)
+)
+
+ +Which we then call in-game with: + +
+/script load foo
+/script in foo invoke run_program
+
+ +However the following code can also be input as a command, or in a command block. + +Since the maximum command that can be input to the chat is limited in length, you will be probably inserting your +programs by pasting them to command blocks or reading from world files, however pasting to command blocks will +remove some whitespaces and squish your newlines making the code not readable. If you are pasting a program that +is perfect and will never cause an error, I salute you, but for the most part it is quite likely that your program +might break, either at compile time, when its initially analyzed, or at execute time, when you suddenly attempt to +divide something by zero. In these cases you would want to get a meaningful error message, but for that you would +need to indicate for the compiler where did you put these new lines, since command block would squish them. For that, +place at the beginning of the line to let the compiler know where are you. This makes so that `$` is the only +character that is illegal in programs, since it will be replaced with new lines. As far as I know, `$` is not +used anywhere inside Minecraft identifiers, so this shouldn't hinder the abilities of your programs. + +Consider the following program executed as command block command: + +
+/script run
+run_program() -> (
+  loop( 10,
+    foo = floor(rand(_));
+    check_not_zero(foo);
+    print(_+' - foo: '+foo);
+    print('  reciprocal: '+  _/foo )
+  )
+);
+check_not_zero(foo) -> (
+   if (foo==0, foo = 1)
+)
+
+ +Lets say that the intention was to check if the bar is zero and prevent division by zero in print, but because +the `foo` is passed as a variable, it never changes the original foo value. Because of the inevitable division +by zero, we get the following message: + +
+Your math is wrong, Incorrect number format for NaN at pos 98
+run_program() -> ( loop( 10, foo = floor(rand(_)); check_not_zero(foo); print(_+' - foo: '+foo);
+HERE>> print(' reciprocal: '+ _/foo ) ));check_not_zero(foo) -> ( if (foo==0, foo = 1))
+
+ +As we can see, we got our problem where the result of the mathematical operation was not a number +(infinity, so not a number), however by pasting our program into the command made it squish the newlines so +while it is clear where the error happened and we still can track the error down, the position of the error (98) +is not very helpful and wouldn't be useful if the program gets significantly longer. To combat this issue we can +precede every line of the script with dollar signs `$`: + +
+/script run
+$run_program() -> (
+$  loop( 10,
+$    foo = floor(rand(_));
+$    check_not_zero(foo);
+$    print(_+' - foo: '+foo);
+$    print('  reciprocal: '+  _/foo )
+$  )
+$);
+$check_not_zero(foo) -> (
+$   if (foo==0, foo = 1)
+$)
+
+ +Then we get the following error message + +
+Your math is wrong, Incorrect number format for NaN at line 7, pos 2
+  print(_+' - foo: '+foo);
+   HERE>> print(' reciprocal: '+ _/foo )
+  )
+
+ +As we can note not only we get much more concise snippet, but also information about the line number and position, +so means its way easier to locate the potential problems problem + +Obviously that's not the way we intended this program to work. To get it `foo` modified via a function call, +we would either return it as a result and assign it to the new variable: + +
+foo = check_not_zero(foo);
+...
+check_not_zero(foo) -> if(foo == 0, 1, foo)
+
+ +.. or convert it to a global variable, which in this case passing as an argument is not required + +
+global_foo = floor(rand(10));
+check_foo_not_zero();
+...
+check_foo_not_zero() -> if(global_foo == 0, global_foo = 1)
+
+ +## Scarpet preprocessor + +There are several preprocessing operations applied to the source of your program to clean it up and prepare for +execution. Some of them will affect your code as it is reported via stack traces and function definition, and some +are applied only on the surface. + - stripping `//` comments (in file mode) + - replacing `$` with newlines (in command mode, modifies submitted code) + - removing extra semicolons that don't follow `;` use as a binary operator, allowing for lenient use of semicolons + - translating `{` into `m(`, `[` into `l(`, and `]` and `}` into `)` + +No further optimizations are currently applied to your code. + +## Mentions + +LR1 parser, tokenizer, and several built-in functions are built based on the EvalEx project. +EvalEx is a handy expression evaluator for Java, that allows to evaluate +simple mathematical and boolean expressions. EvalEx is distributed under MIT licence. +For more information, see: [EvalEx GitHub repository](https://github.com/uklimaschewski/EvalEx) +# Variables and Constants + +`scarpet` provides a number of constants that can be used literally in scripts + +* `null`: nothing, zilch, not even false +* `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 + +Apart from that, there is a bunch of system variables, that start with `_` that are set by `scarpet` built-ins, +like `_`, typically each consecutive value in loops, `_i` indicating iteration, or `_a` like an accumulator +for `reduce` function. Certain calls to Minecraft specific calls would also set `_x`, `_y`, `_z`, indicating +block positions. All variables starting with `_` are read-only, and cannot be declared and modified in client code. + +## Literals + +`scarpet` accepts numeric and string liters constants. Numbers look like `1, 2.5, -3e-7, 0xff,` and are internally +represented primarily as Java's `double` but `scarpet` will try to trim trailing zeros as much as possible so if you +need to use them as integers or even longs - you can. Long values will also not loose their long precision in addition, +subtraction, negation and multiplication, however any other operation that is not guaranteed to return a long value +(like division) on a number even if it can be properly +represented as long, will make them convert to doubles. + +Strings use single quoting, for multiple reasons, but primarily to allow for +easier use of strings inside doubly quoted command arguments (when passing a script as a parameter of `/script fill` +for example), or when typing in jsons inside scarpet (to feed back into a `/data merge` command for example). +Strings also use backslashes `\` for quoting special characters, in both plain strings and regular expressions + +
+'foo'
+print('This doesn\'t work')
+nbt ~ '\\.foo'   // matching '.' as a '.', not 'any character match'
+
+# Operators + +There is a number of operators you can use inside the expressions. Those could be considered generic type operators +that apply to most data types. They also follow standard operator precedence, i.e. `2+2*2` is understood +as `2+(2*2)`, not `(2+2)*2`, otherwise they are applied from left to right, i.e. `2+4-3` is interpreted +as `(2+4)-3`, which in case of numbers doesn't matter, but since `scarpet` allows for mixing all value types +the associativity would matter, and may lead to unintended effects: + +Operators can be unary - with one argument prefixed by the operator (like `-`, `!`, `...`), "practically binary" (that +clearly have left and right operands, like assignment `=` operator), and "technically binary" (all binary operators have left and +right hand, but can be frequently chained together, like `1+2+3`). All "technically binary" operators (where chaining makes sense) +have their functional counterparts, e.g. `1+2+3` is equivalent to `sum(1, 2, 3)`. Functional and operatoral forms are directly +equivalent - they actually will result in the same code as scarpet will optimize long operator chains into their optimized functional forms. + +Important operator is function definition `->` operator. It will be covered +in [User Defined Functions and Program Control Flow](docs/scarpet/language/FunctionsAndControlFlow.md) + +
+'123'+4-2 => ('123'+4)-2 => '1234'-2 => '134'
+'123'+(4-2) => '123'+2 => '1232'
+3*'foo' => 'foofoofoo'
+1357-5 => 1352
+1357-'5' => 137
+3*'foo'-'o' => 'fff'
+[1,3,5]+7 => [8,10,12]
+
+ +As you can see, values can behave differently when mixed with other types in the same expression. +In case values are of the same types, the result tends to be obvious, but `Scarpet` tries to make sense of whatever +it has to deal with + +## Operator Precedence + +Here is the complete list of operators in `scarpet` including control flow operators. Note, that commas and brackets +are not technically operators, but part of the language, even if they look like them: + +* Match, Get `~ :` +* Unary `+ - ! ...` +* Exponent `^` +* Multiplication `* / %` +* Addition `+ -` +* Comparison `> >= <= <` +* Equality `== !=` +* Logical And`&&` +* Logical Or `||` +* Assignment `= += <>` +* Definition `->` +* Next statement`;` +* Comma `,` +* Bracket `( )` + +### `Get, Accessor Operator :` + +Operator version of the `get(...)` function to access elements of lists, maps, and potentially other containers +(i.e. NBTs). It is important to distinguish from `~` operator, which is a matching operator, which is expected to +perform some extra computations to retrieve the result, while `:` should be straightforward and immediate, and +the source object should behave like a container and support full container API, +meaning `get(...)`, `put(...)`, `delete(...)`, and `has(...)` functions + +For certain operators and functions (get, put, delete, has, =, +=) objects can use `:` annotated fields as l-values, +meaning construct like `foo:0 = 5`, would act like `put(foo, 0, 5)`, rather than `get(foo, 0) = 5`, +which would result in an error. + +TODO: add more information about l-value behaviour. + +### `Matching Operator ~` + +This operator should be understood as 'matches', 'contains', 'is_in', or 'find me some stuff about something else. +For strings it matches the right operand as a regular expression to the left, returning: + - `null` if there is no match + - matched phrase if no grouping is applied + - matched element if one group is applied + - list of matches if more than one grouping is applied + +This can be used to extract information from unparsed nbt's in a more convoluted way (use `get(...)` for +more appropriate way of doing it). For lists it checks if an element is in the list, and returns the +index of that element, or `null` if no such element was found, especially that the use of `first(...)` +function will not return the index. Currently it doesn't have any special behaviour for numbers - it checks for +existence of characters in string representation of the left operand with respect of the regular expression on +the right hand side. + +In Minecraft API portion `entity ~ feature` is a shortcode for `query(entity,feature)` for queries that do not take +any extra arguments. + +
+[1,2,3] ~ 2  => 1
+[1,2,3] ~ 4  => null
+
+'foobar' ~ 'baz'  => null
+'foobar' ~ '.b'  => 'ob'
+'foobar' ~ '(.)b'  => 'o'
+'foobar' ~ '((.)b)'  => ['ob', 'o']
+'foobar' ~ '((.)(b))'  => ['ob', 'o', 'b']
+'foobar' ~ '(?:(.)(?:b))'  => 'o'
+
+player('*') ~ 'gnembon'  // null unless player gnembon is logged in (better to use player('gnembon') instead
+p ~ 'sneaking' // if p is an entity returns whether p is sneaking
+
+ +Or a longer example of an ineffective way to searching for a squid + +
+entities = entities_area('all',x,y,z,100,10,100);
+sid = entities ~ 'Squid';
+if(sid != null, run('execute as '+query(get(entities,sid),'id')+' run say I am here '+query(get(entities,sid),'pos') ) )
+
+ +Or an example to find if a player has specific enchantment on a held axe (either hand) and get its level +(not using proper NBTs query support via `get(...)`): + +
+global_get_enchantment(p, ench) -> (
+$   for(['mainhand','offhand'],
+$      holds = query(p, 'holds', _);
+$      if( holds,
+$         [what, count, nbt] = holds;
+$         if( what ~ '_axe' && nbt ~ ench,
+$            lvl = max(lvl, number(nbt ~ '(?<=lvl:)\\d') )
+$         )
+$      )
+$   );
+$   lvl
+$);
+/script run global_get_enchantment(player(), 'sharpness')
+
+ +### Basic Arithmetic Operators `+`, `sum(...)`, `-`, `difference(...)`, `*`, `product(...)`, `/`, `quotient(...)` + +Allows to add the results of two expressions. If the operands resolve to numbers, the result is arithmetic operation. +In case of strings, adding or subtracting from a string results in string concatenation and removal of substrings +from that string. Multiplication of strings and numbers results in repeating the string N times and division results +in taking the first k'th part of the string, so that `str*n/n ~ str` + +In case first operand is a list, either it +results in a new list with all elements modified one by one with the other operand, or if the operand is a list +with the same number of items - element-wise addition/subtraction. This prioritize treating lists as value containers +to lists treated as vectors. + +Addition with maps (`{}` or `m()`) results in a new map with keys from both maps added, if both operands are maps, +adding elements of the right argument to the keys, of left map, or just adding the right value as a new key +in the output map. + +Functional forms of `-` and `/` have less intuitive multi-nary interpretation, but they might be useful in some situations. +`x-y-z` resolves to `difference(x, y, z)`. + +`/` always produces a properly accurate result, fully reversible with `*` operator. To obtain a integer 'div' result, use +`floor(x/y)`. + +Examples: + +
+2+3 => 5
+'foo'+3+2 => 'foo32'
+'foo'+(3+2) => 'foo5'
+3+2+'bar' => '5bar'
+'foo'*3 => 'foofoofoo'
+'foofoofoo' / 3 => 'foo'
+'foofoofoo'-'o' => 'fff'
+[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}
+
+ +### Just Operators `%`, `^` + +The modulo and exponent (power) operators work only if both operands are numbers. `%` is a proper (and useful) 'modulus' operator, +not a useless 'reminder' operator that you would expect from anything that touches Java. While typically modulus is reserved +to integer numbers, scarpet expands them to floats with as much sense as possible. + +
pi^pi%euler  => 1.124....
+-9 % 4  => 3
+9 % -4  => -3
+9.1 % -4.2  => -3.5
+9.1 % 4.2  => 0.7
+-3 ^ 2  => 9
+-3 ^ pi => // Error
+
+ +### Comparison Operators `==`, `equal()`, `!=`, `unique()`, `<`, `increasing()`, `>`, `decreasing()`, `<=`, `nondecreasing()`, `>=`, `nonincreasing()` + +Allows to compare the results of two expressions. For numbers, it considers arithmetic order of numbers, for +strings - lexicographical, nulls are always 'less' than everything else, and lists check their elements - +if the sizes are different, the size matters, otherwise, pairwise comparisons for each element are performed. +The same order rules than with all these operators are used with the default sortographical order as used by `sort` +function. All of these are true: + +
+null == null
+null != false
+0 == false
+1 == true
+null < 0
+null < -1000
+1000 < 'a'
+'bar' < 'foo'
+3 == 3.0
+
+ +Functional variants of these operators allow to assert certain paradigms on multiple arguments at once. This means that +due to formal equivalence `x < y < z` is equivalent to `x < y & y < z` because of direct mapping to `increasing(x, y, z)`. This translates through +the parentheses, so `((x < y) < z)` is the same as `increasing(x, y, z)`. To achieve the same effect as you would see in other + languages (not python), you would need to cast the first pair to boolean value, i.e. `bool(x < y) < z`. + +### Logical Operators `&&`, `and(...)`, `||`, `or(...)` + +These operator compute respective boolean operation on the operands. What it important is that if calculating of the +second operand is not necessary, it won't be evaluated, which means one can use them as conditional statements. In +case of success returns first positive operand (`||`) or last one (`&&`). + +
+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
+
+ +### `Assignment Operators = <> +=` + +A set of assignment operators. All require bounded variable on the LHS, `<>` requires bounded arguments on the +right hand side as well (bounded, meaning being variables). Additionally they can also handle list constructors +with all bounded variables, and work then as list assignment operators. When `+=` is used on a list, it extends +that list of that element, and returns the list (old == new). `scarpet` doesn't support currently removal of items. +Removal of items can be obtained via `filter` command, and reassigning it fo the same variable. Both operations would +require rewriting of the array anyways. + +
+a = 5  => a == 5
+[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]
+
+ +### `Unary Operators - +` + +Require a number, flips the sign. One way to assert it's a number is by crashing the script. gg. + +
+-4  => -4
++4  => 4
++'4'  // Error message
+
+ +### `Negation Operator !` + +flips boolean condition of the expression. Equivalent of `bool(expr)==false` + +
+!true  => false
+!false  => true
+!null  => true
+!5  => false
+![] => true
+![null] => false
+
+ +### `Unpacking Operator ...` + +Unpacks elements of a list of an iterator into a sequence of arguments in a function making so that `fun(...[1, 2, 3])` is +identical with `fun(1, 2, 3)`. For maps, it unpacks them to a list of key-value pairs. + +In function signatures it identifies a vararg parameter. + +
+fun(a, b, ... rest) -> [a, b, rest]; fun(1, 2, 3, 4)    => [1, 2, [3, 4]]
+
+ +Effects of `...` can be surprisingly lasting. It is kept through the use of variables and function calls. + +
+fun(a, b, ... rest) -> [a, b, ... rest]; fun(1, 2, 3, 4)    => [1, 2, 3, 4]
+args() -> ... [1, 2, 3]; sum(a, b, c) -> a+b+c; sum(args())   => 6
+a = ... [1, 2, 3]; sum(a, b, c) -> a+b+c; sum(a)   => 6
+
+ +Unpacking mechanics can be used for list and map construction, not just for function calls. + +
+[...range(5), pi, ...range(5,-1,-1)]   => [0, 1, 2, 3, 4, 3.14159265359, 5, 4, 3, 2, 1, 0]
+{ ... map(range(5),  _  -> _*_ )}   => {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
+{...{1 -> 2, 3 -> 4}, ...{5 -> 6, 7 -> 8}}   => {1: 2, 3: 4, 5: 6, 7: 8}
+
+ +Fine print: unpacking of argument lists happens just before functions are evaluated. +This means that in some situations, for instance +when an expression is expected (`map(list, expr)`), or a function should not evaluate some (most!) of its arguments (`if(...)`), +unpacking cannot be used, and will be ignored, leaving `... list` identical to `list`. +Functions that don't honor unpacking mechanics, should have no use for it at the first place + (i.e. have one, or very well-defined, and very specific parameters), +so some caution (prior testing) is advised. Some of these multi-argument built-in functions are + `if`, `try`, `sort_key`, `system_variable_get`, `synchronize`, `sleep`, `in_dimension`, +all container functions (`get`, `has`, `put`, `delete`), +and all loop functions (`while`, `loop`, `map`, `filter`, `first`, `all`, `c_for`, `for` and`reduce`). + +### `Binary (bitwise) operations` + +These are a bunch of operators that work exclusively on numbers, more specifically their binary representations. Some of these +work on multiple numbers, some on only 2, and others on only 1. Note that most of these functions (all but `double_to_long_bits`) +only take integer values, so if the input has a decimal part, it will be discarded. + + - `bitwise_and(...)` -> Does the bitwise AND operation on each number in order. Note that with larger ranges of numbers this will + tend to 0. + - `bitwise_xor(...)` -> Does the bitwise XOR operation on each number in order. + - `bitwise_or(...)` -> Does the bitwise AND operation on each number in order. Note that with larger ranges of numbers this will + tend to -1. + - `bitwise_shift_left(num, amount)` -> Shifts all the bits of the first number `amount` spots to the left. Note that only the 6 + lowest-order bits of the amount are considered. + - `bitwise_shift_right(num, amount)` -> Shifts all the bits of the first number `amount` spots to the right logically. That is, the + `amount` most significant bits will always be set to 0. Like with the above, only the 6 lowest-order bits of the amount are considered. + - `bitwise_arithmetic_shift_right(num, amount)` -> Shifts all the bits of the first number `amount` spots to the right arithmetically. + That is, if the most significant (sign) bit is a 1, it'll propagate the one to the `amount` most significant bits. Like with the above, + only the 6 lowest-order bits of the amount are considered. + - `bitwise_roll_left(num, amount)` -> Rolls the bits of the first number `amount` bits to the left. This is basically where you + shift out the first `amount` bits and then add them on at the back, essentially 'rolling' the number. Note that unlike with + shifting, you can roll more than 63 bits at a time, as it just makes the number roll over more times, which isn't an issue + - `bitwise_roll_right(num, amount)` -> Same as above, just rolling in the other direction + - `bitwise_not(num)` -> Flips all the bits of the number. This is simply done by performing xor operation with -1, which in binary is + all ones. + - `bitwise_popcount(num)` -> Returns the number of ones in the binary representation of the number. For the number of zeroes, just + do 64 minus this number. + - `double_to_long_bits(num)` -> Returns a representation of the specified floating-point value according to the IEEE 754 floating-point + "double format" bit layout. + - `long_to_double_bits(num)` -> Returns the double value corresponding to a given bit representation. +# Arithmetic operations + +## Basic Arithmetic Functions + +There is bunch of them - they require a number and spit out a number, doing what you would expect them to do. + +### `fact(n)` + +Factorial of a number, a.k.a `n!`, just not in `scarpet`. Gets big... quick... Therefore, values larger +than `fact(20)` will not return the exact value, but a value with 'double-float' precision. + +### `sqrt(n)` + +Square root (not 'a squirt') of a number. For other fancy roots, use `^`, math and yo noggin. Imagine square roots on a tree... + +### `abs(n)` + +Absolut value. + +### `round(n)` + +Closest integer value. Did you know the earth is also round? + +### `floor(n)` + +Highest integer that is still no larger then `n`. Insert a floor pun here. + +### `ceil(n)` + +First lucky integer that is not smaller than `n`. As you would expect, ceiling is typically right above the floor. + +### `ln(n)` + +Natural logarithm of `n`. Naturally. + +### `ln1p(n)` + +Natural logarithm of `n+1`. Very optimistic. + +### `log10(n)` + +Decimal logarithm of `n`. Its ceiling is the length of its floor. + +### `log(n)` + +Binary logarithm of `n`. Finally, a proper one, not like the previous 11. + +### `log1p(n)` + +Binary logarithm of `n+1`. Also always positive. + +### `mandelbrot(a, b, limit)` + +Computes the value of the mandelbrot set, for set `a` and `b` spot. Spot the beetle. Why not. + +### `min(arg, ...), min(list), max(arg, ...), max(list)` + +Compute minimum or maximum of supplied arguments assuming default sorthoraphical order. +In case you are missing `argmax`, just use `a ~ max(a)`, little less efficient, but still fun. + +Interesting bit - `min` and `max` don't remove variable associations from arguments, which means can be used as +LHS of assignments (obvious case), or argument spec in function definitions (far less obvious). + +
+a = 1; b = 2; min(a,b) = 3; [a,b]  => [3, 2]
+a = 1; b = 2; fun(x, min(a,b)) -> [a,b]; fun(3,5)  => [5, 0]
+
+ +Absolutely no idea, how the latter might be useful in practice. But since it compiles, can ship it. + +### `relu(n)` + +Linear rectifier of `n`. 0 below 0, n above. Why not. `max(0,n)` with less moral repercussions. + +## Trigonometric / Geometric Functions + +### `sin(x)` + +### `cos(x)` + +### `tan(x)` + +### `asin(x)` + +### `acos(x)` + +### `atan(x)` + +### `atan2(x,y)` + +### `sinh(x)` + +### `cosh(x)` + +### `tanh(x)` + +### `sec(x)` + +### `csc(x)` + +### `sech(x)` + +### `csch(x)` + +### `cot(x)` + +### `acot(x)` + +### `coth(x)` + +### `asinh(x)` + +### `acosh(x)` + +### `atanh(x)` + +### `rad(deg)` + +### `deg(rad)` + +Use as you wish +# System functions + +## Type conversion functions + +### `copy(expr)` + +Returns the deep copy of the expression. Can be used to copy mutable objects, like maps and lists + +### `type(expr)` + +Returns the string value indicating type of the expression. Possible outcomes +are `null`, `number`, `string`, `list`, `map`, `iterator`, `function`, `task`, +as well as minecraft related concepts like `block`, `entity`, `nbt`, `text`. + +### `bool(expr)` + +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. + +
+bool(pi) => true
+bool(false) => false
+bool('') => false
+bool([]) => false
+bool(['']) => true
+bool('foo') => true
+bool('false') => false
+bool('nulL') => false
+if('false',1,0) => true
+
+ +### `number(expr)` + +Returns a numeric context of the expression. Can be used to read numbers from strings, or other types + +
+number(null) => 0
+number(false) => 0
+number(true) => 1
+number('') => null
+number('3.14') => 3.14
+number([]) => 0
+number(['']) => 1
+number('foo') => null
+number('3bar') => null
+number('2')+number('2') => 4
+
+ +### `str(expr)`,`str(expr, params? ... )`, `str(expr, param_list)` + +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', [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'
+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'
+
+ +* * * +## Threading and Parallel Execution + +Scarpet allows to run threads of execution in parallel to the main script execution thread. In Minecraft, apps +are executed on the main server thread. Since Minecraft is inherently NOT thread safe, it is not that +beneficial to parallel execution in order to access world resources faster. Both `getBlockState` and `setBlockState` +are not thread safe and require the execution to park on the server thread, where these requests can be executed in +the off-tick time in between ticks that didn't take all 50ms. There are however benefits of running things in parallel, +like fine time control not relying on the tick clock, or running things independent on each other. You can still run +your actions on tick-by-tick basis, either taking control of the execution using `game_tick()` API function +(nasty solution), or scheduling tick using `schedule()` function (preferred solution), but threading gives much more control +on the timings without impacting the main game and is the only solution to solve problems in parallel +(see [scarpet camera](/src/main/resources/assets/carpet/scripts/camera.sc)). + +Due to limitations with the game, there are some limits to the threading as well. You cannot for +instance `join_task()` at all from the main script and server thread, because any use of Minecraft specific +function that require any world access, will require to park and join on the main thread to get world access, +meaning that calling join on that task would inevitably lead to a typical deadlock. You can still join tasks +from other threads, just because the only possibility of a deadlock in this case would come explicitly from your +bad code, not the internal world access behaviour. Some things tough like players or entities manipulation, can be +effectively parallelized. + +If the app is shutting down, creating new tasks via `task` will not succeed. Instead the new task will do nothing and return +`null`, so most threaded application should handle closing apps naturally. Keep in mind in case you rely on task return values, +that they will return `null` regardless of anything in these situations. When app handles `__on_close()` event, new tasks cannot +be submitted at this point, but current tasks are not terminated. Apps can use that opportunity to gracefully shutdown their tasks. +Regardless if the app handles `__on_close()` event, or does anything with their tasks in it, all tasks will be terminated exceptionally +within the next 1.5 seconds. + +### `task(function, ... args)`, `task_thread(executor, function, ... args)` + +Creates and runs a parallel task, returning the handle to the task object. Task will return the return value of the +function when its completed, or will return `null` immediately if task is still in progress, so grabbing a value of +a task object is non-blocking. Function can be either function value, or function lambda, or a name of an existing +defined function. In case function needs arguments to be called with, they should be supplied after the function +name, or value. `executor` identifier in `task_thread`, places the task in a specific queue identified by this value. +The default thread value is the `null` thread. There are no limits on number of parallel tasks for any executor, +so using different queues is solely for synchronization purposes. + +
+task( _() -> print('Hello Other World') )  => Runs print command on a separate thread
+foo(a, b) -> print(a+b); task('foo',2,2)  => Uses existing function definition to start a task
+task_thread('temp', 'foo',3,5);  => runs function foo with a different thread executor, identified as 'temp'
+a = 3; task_thread('temp', _(outer(a), b) -> foo(a,b), 5)  
+    => Another example of running the same thing passing arguments using closure over anonymous function as well as passing a parameter.
+
+ +In case you want to create a task based on a function that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + +### `sleep()` `sleep(timeout)`, `sleep(timeout, close_expr)` + + +Halts the execution of the thread (or the game itself, if run not as a part of a task) for `expr` milliseconds. +It checks for interrupted execution, in that case exits the thread (or the entire program, if not run on a thread) in case the app +is being stopped/removed. If the closing expression is specified, executes the expression when a shutdown signal is triggered. +If run on the main thread (i.e. not as a task) the close expression may only be invoked when the entire game shuts down, so close call only +makes sense for threads. For regular programs, use `__on_close()` handler. + +Since `close_expr` is executed after app shutdown is initiated, you won't be able to create new tasks in that block. Threads +should periodically call `sleep` to ensure all app tasks will finish when the app is closing or right after, but the app engine +will not forcefully remove your running tasks, so the tasks themselves need to properly react to the closing request. + +
+sleep(50)  # wait for 50 milliseconds
+sleep(1000, print('Interrupted')) # waits for 1 second, outputs a message when thread is shut down.
+
+ +### `task_count(executor?)` + +If no argument provided, returns total number of tasks being executed in parallel at this moment using scarpet +threading system. If the executor is provided, returns number of active tasks for that provider. Use `task_count(null)` +to get the task count of the default executor only. + +### `task_value(task)` + +Returns the task return value, or `null` if task hasn't finished yet. Its a non-blocking operation. Unlike `join_task`, +can be called on any task at any point + +### `task_join(task)` + +Waits for the task completion and returns its computed value. If the task has already finished returns it immediately. +Unless taking the task value directly, i.e. via `task_value`, this operation is blocking. Since Minecraft has a +limitation that all world access operations have to be performed on the main game thread in the off-tick time, +joining any tasks that use Minecraft API from the main thread would mean automatic lock, so joining from the main +thread is not allowed. Join tasks from other threads, if you really need to, or communicate asynchronously with +the task via globals or function data / arguments to monitor its progress, communicate, get partial results, +or signal termination. + +### `task_completed(task)` + +Returns true if task has completed, or false otherwise. + +### `synchronize(lock, expression)` + +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 + +### `lower(expr), upper(expr), title(expr)` + +Returns lowercase, uppercase or titlecase representation of a string representation of the passed expression + +
+lower('aBc') => 'abc'
+upper('aBc') => 'ABC'
+title('aBc') => 'Abc'
+
+ +### `replace(string, regex, repl?); replace_first(string, regex, repl?)` + +Replaces all, or first occurrence of a regular expression in the string with `repl` expression, +or nothing, if not specified. To use escape characters (`\(`,`\+`,...), metacharacters (`\d`,`\w`,...), or position anchors (`\b`,`\z`,...) in your regular expression, use two backslashes. + +
+replace('abbccddebfg','b+','z')  // => azccddezfg
+replace('abbccddebfg','\\w$','z')  // => abbccddebfz
+replace_first('abbccddebfg','b+','z')  // => azccddebfg
+
+ +### `length(expr)` + +Returns length of the expression, the length of the string, the length of the integer part of the number, +or length of the list + +
+length(pi) => 1
+length(pi*pi) => 1
+length(pi^pi) => 2
+length([]) => 0
+length([1,2,3]) => 3
+length('') => 0
+length('foo') => 3
+
+ +### `rand(expr), rand(expr, seed)` + +returns a random number from `0.0` (inclusive) to `expr` (exclusive). In boolean context (in conditions, +boolean functions, or `bool`), returns false if the randomly selected value is less than 1. This means +that `bool(rand(2))` returns true half of the time and `!rand(5)` returns true for 20% (1/5) of the time. If seed is not +provided, uses a random seed that's shared across all scarpet apps. +If seed is provided, each consecutive call to rand() will act like 'next' call to the +same random object. Scarpet keeps track of up to 65536 custom random number generators (custom seeds, per app), +so if you exceed this number, your random sequences will revert to the beginning and start over. + +
+map(range(10), floor(rand(10))) => [5, 8, 0, 6, 9, 3, 9, 9, 1, 8]
+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]
+
+ +## `reset_seed(seed)` + +Resets the sequence of the randomizer used by `rand` for this seed to its initial state. Returns a boolean value +indicating if the given seed has been used or not. + +### `perlin(x), perlin(x, y), perlin(x, y, z), perlin(x, y, z, seed)` + +returns a noise value from `0.0` to `1.0` (roughly) for 1, 2 or 3 dimensional coordinate. The default seed it samples +from is `0`, but seed can be specified as a 4th argument as well. In case you need 1D or 2D noise values with custom +seed, use `null` for `y` and `z`, or `z` arguments respectively. + +Perlin noise is based on a square grid and generates rougher maps comparing to Simplex, which is creamier. +Querying for lower-dimensional result, rather than affixing unused dimensions to constants has a speed benefit, + +Thou shall not sample from noise changing seed frequently. Scarpet will keep track of the last 256 perlin seeds +used for sampling providing similar speed comparing to the default seed of `0`. In case the app engine uses more +than 256 seeds at the same time, switching between them can get much more expensive. + +### `simplex(x, y), simplex(x, y, z), simplex(x, y, z, seed)` + +returns a noise value from `0.0` to `1.0` (roughly) for 2 or 3 dimensional coordinate. The default seed it samples +from is `0`, but seed can be specified as a 4th argument as well. In case you need 2D noise values with custom seed, +use `null` for `z` argument. + +Simplex noise is based on a triangular grid and generates smoother maps comparing to Perlin. To sample 1D simplex +noise, affix other coordinate to a constant. + +Thou shall not sample from noise changing seed frequently. Scarpet will keep track of the last 256 simplex seeds +used for sampling providing similar speed comparing to the default seed of `0`. In case the app engine uses more +than 256 seeds at the same time, switching between them can get much more expensive. + +### `print(expr)`, `print(player, expr)` + +prints the value of the expression to chat. Passes the result of the argument to the output unchanged, +so `print`-statements can be weaved in code to debug programming issues. By default it uses the same communication +channels that most vanilla commands are using. + +In case player is directly specified, it only sends the message to that player, like `tell` command. + +
+print('foo') => results in foo, prints: foo
+a = 1; print(a = 5) => results in 5, prints: 5
+a = 1; print(a) = 5 => results in 5, prints: 1
+print('pi = '+pi) => prints: pi = 3.141592653589793
+print(str('pi = %.2f',pi)) => prints: pi = 3.14
+
+ +### `time()` + +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();
+flip_my_world_upside_down();
+print(str('this took %d milliseconds',time()-start_time))
+
+ +### `unix_time()` + +Returns standard POSIX time as a number of milliseconds since the start of the epoch +(00:00 am and 0 seconds, 1 Jan 1970). +Unlike the previous function, this can be used to get exact time, but it varies from time zone to time zone. + +### `convert_date(milliseconds)` +### `convert_date(year, month, date, hours?, mins?, secs?)` +### `convert_date([year, month, date, hours?, mins?, secs?])` + +If called with a single argument, converts standard POSIX time to a list in the format: + +`[year, month, date, hours, mins, secs, day_of_week, day_of_year, week_of_year]` + +eg: `convert_date(1592401346960) -> [2020, 6, 17, 10, 42, 26, 3, 169, 25]` + +Where the `6` stands for June, but `17` stands for 17th, `10` stands for 10am, +`42` stands for 42 minutes past the hour, and `26` stands for 26 seconds past the minute, +and `3` stands for Wednesday, `169` is the day of year, and `25` is a week of year. + +Run `convert_date(unix_time())` to get current time as list. + + +When called with a list, or with 3 or 6 arguments, returns standard POSIX time as a number of milliseconds since the + start of the epoch (1 Jan 1970), +using the time inputted into the function as opposed to the system time. + +Example editing: +
+date = convert_date(unix_time());
+
+months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+
+days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
+
+print(
+  str('Its %s, %d %s %d, %02d:%02d:%02d', 
+    days:(date:6-1), date:2, months:(date:1-1), date:0, date:3, date:4, date:5 
+  )
+)  
+
+ +This will give you a date: + +It is currently `hrs`:`mins` and `secs` seconds on the `date`th of `month`, `year` + +### `encode_b64(string)`, `decode_b64(string)` + +Encode or decode a string from b64, throwing a `b64_error` exception if it's invalid + +### `encode_json(value)`, `decode_json(string)` + +Encodes a value as a json string, and decodes a json string as a valid value, throwing a `json_error` exception if it +doesn't parse properly + +### `profile_expr(expression)` + +Returns number of times given expression can be run in 50ms time. Useful to profile and optimize your code. +Note that, even if its only a number, it WILL run these commands, so if they are destructive, you need to be careful. + +* * * + +## Access to variables and stored functions (use with caution) + +### `var(expr)` + +Returns the variable under the name of the string value of the expression. Allows to manipulate variables in more +programmatic manner, which allows to use local variable set with a hash map type key-value access, +can also be used with global variables + +
+a = 1; var('a') = 'foo'; a => a == 'foo'
+
+ +### `undef(expr)` + +Removes all bindings of a variable with a name of `expr`. Removes also all function definitions with that name. +It can affect global variable pool, and local variable set for a particular function. + +
+inc(i) -> i+1; foo = 5; inc(foo) => 6
+inc(i) -> i+1; foo = 5; undef('foo'); inc(foo) => 1
+inc(i) -> i+1; foo = 5; undef('inc'); undef('foo'); inc(foo) => Error: Function inc is not defined yet at pos 53
+
+ +### `vars(prefix)` + +It returns all names of variables from local scope (if prefix does not start with 'global') or global variables +(otherwise). Here is a larger example that uses combination of `vars` and `var` functions to be +used for object counting + +
+/script run
+$ count_blocks(ent) -> (
+$   [cx, cy, cz] = query(ent, 'pos');
+$   scan(cx, cy, cz, 16, 16, 16, var('count_'+_) += 1);
+$   for ( sort_key( vars('count_'), -var(_)),
+$     print(str( '%s: %d', slice(_,6), var(_) ))
+$   )
+$ )
+/script run count_blocks(player())
+
+ +* * * + +## System key-value storage + +Scarpet runs apps in isolation. The can share code via use of shared libraries, but each library that is imported to +each app is specific to that app. Apps can store and fetch state from disk, but its restricted to specific locations +meaning apps cannot interact via disk either. To facilitate communication for interappperability, scarpet hosts its +own key-value storage that is shared between all apps currently running on the host, providing methods for getting an +associated value with optional setting it if not present, and an operation of modifying a content of a system +global value. + +### `system_variable_get(key, default_value ?)` + +Returns the variable from the system shared key-value storage keyed with a `key` value, optionally if value is +not present, and default expression is provided, sets a new value to be associated with that key + +### `system_variable_set(key, new_value)` + +Returns the variable from the system shared key-value storage keyed with a `key` value, and sets a new +mapping for the key +# Loops, and higher order functions + +Efficient use of these functions can greatly simplify your programs and speed them up, as these functions will +internalize most of the operations that need to be applied on multiple values at the same time. Most of them take +a `list` argument which can be any iterable structure in scarpet, including generators, like `rect`, or `range`, +and maps, where the iterator returns all the map keys + +## Loops + +### `break(), break(expr), continue(), continue(expr)` + +These allow to control execution of a loop either skipping current iteration code, using `continue`, or finishing the +current loop, using `break`. `break` and `continue` can only be used inside `for`, `c_for`, `while`, `loop`, `map`, +`filter`, `reduce` as well as Minecraft API block loops, `scan` and `volume` +functions, while `break` can be used in `first` as well. Outside of the internal expressions of these functions, +calling `break` or `continue` will cause an error. In case of the nested loops, and more complex setups, use +custom `try` and `throw` setup. + +Please check corresponding loop function description what `continue` and `break` do in their contexts, but in +general case, passed values to `break` and `continue` will be used in place of the return value of the internal +iteration expression. + +### `c_for(init, condition, increment, body)` + +`c_for` Mimics c-style tri-arg (plus body) for loops. Return value of `c_for` is number of iterations performed in the + loop. Unlike other loops, the `body` is not provided with pre-initialized `_` style variables - all initialization + and increments has to be handled by the programmers themselves. + `break` and `continue` statements are handled within `body` expression only, and not in `condition` or `increment`. + +
+ c_for(x=0, x<10, x+=1,
+    c_for(y=0, y<10, y+=1,
+        print(str('%d * %d = %d', x, y, x*y))
+    )
+ )
+ 
+ +### `for(list,expr(_,_i))` + +Evaluates expression over list of items from the `list`. Supplies `_`(value) and `_i`(iteration number) to the `expr`. + +Returns the number of times `expr` was successful. Uses `continue` and `break` argument in place of the returned +value from the `expr`(if supplied), to determine if the iteration was successful. + +
+check_prime(n) -> !first( range(2, sqrt(n)+1), !(n % _) );
+for(range(1000000,1100000),check_prime(_))  => 7216
+
+ +From which we can learn that there is 7216 primes between 1M and 1.1M + +### `while(cond, expr)`, `while(cond, limit, expr)` + +Evaluates expression `expr` repeatedly until condition `cond` becomes false, but not more than `limit` times (if limit is specified). +Returns the result of the last `expr` evaluation, or `null` if nothing was successful. Both `expr` and `cond` will +received a bound variable `_` indicating current iteration, so its a number. + +
+while(a<100,a=_*_) => 100 // loop stopped at condition
+while(a<100,10,a=_*_)  => 81 // loop exhausted via limit
+while(a<100,20,a=_*_)  => 100 // loop stopped at condition, but a has already been assigned
+while(_*_<100,20,a=_*_)  => 81 // loop stopped at condition, before a was assigned a value
+
+ +### `loop(num,expr(_),exit(_)?)` + +Evaluates expression `expr`, `num` number of times. code>expr receives `_` system variable indicating the iteration. + +
+loop(5, game_tick())  => repeat tick 5 times
+list = []; loop(5, x = _; loop(5, list += [x, _] ) ); list
+  // double loop, produces: [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [1, 0], [1, 1], ... , [4, 2], [4, 3], [4, 4]]
+
+ +In this small example we will search for first 10 primes, apparently including 0: + +
+check_prime(n) -> !first( range(2, sqrt(n)+1), !(n % _) );
+primes = [];
+loop(10000, if(check_prime(_), primes += _ ; if (length(primes) >= 10, break())));
+primes
+// outputs: [0, 1, 2, 3, 5, 7, 11, 13, 17, 19]
+
+ +## Higher Order Functions + +### `map(list,expr(_,_i))` + +Converts a `list` of values, to another list where each value is result of an expression `v = expr(_, _i)` +where `_` is passed as each element of the list, and `_i` is the index of such element. If `break` is called the +map returns whatever collected thus far. If `continue` and `break` are used with supplied argument, it is used in +place of the resulting map element, otherwise current element is skipped. + +
+map(range(10), _*_)  => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
+map(player('*'), _+' is stoopid') [gnembon is stoopid, herobrine is stoopid]
+
+ +### `filter(list,expr(_,_i))` + +filters `list` elements returning only these that return positive result of the `expr`. With `break` and `continue` +statements, the supplied value can be used as a boolean check instead. + +
+filter(range(100), !(_%5), _*_>1000)  => [0, 5, 10, 15, 20, 25, 30]
+map(filter(entity_list('*'),_=='Witch'), query(_,'pos') )  => [[1082.5, 57, 1243.5]]
+
+ +### `first(list,expr(_,_i))` + +Finds and returns the first item in the list that satisfies `expr`. It sets `_` for current element value, +and `_i` for index of that element. `break` can be called inside the iteration code, using its argument value +instead of the current item. `continue` has no sense and cannot be called inside `first` call. + +
+first(range(1000,10000), n=_; !first( range(2, sqrt(n)+1), !(n % _) ) )  => 1009 // first prime after 1000
+
+ +Notice in the example above, that we needed to rename the outer `_` to be albe to use in in the inner `first` call + +### `all(list,expr(_,_i))` + +Returns `true` if all elements on the list satisfy the condition. Its roughly equivalent +to `all(list,expr) <=> for(list,expr)==length(list)`. `expr` also receives bound `_` and `_i` variables. `break` +and `continue` have no sense and cannot be used inside of `expr` body. + +
+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), [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
+
+ +### `reduce(list,expr(_a,_,_i), initial)` + +Applies `expr` for each element of the list and saves the result in `_a` accumulator. Consecutive calls to `expr` +can access that value to apply more values. You also need to specify the initial value to apply for the +accumulator. `break` can be used to terminate reduction prematurely. If a value is provided to `break` or `continue`, +it will be used from now on as a new value for the accumulator. + +
+reduce([1,2,3,4],_a+_,0)  => 10
+reduce([1,2,3,4],_a*_,1)  => 24
+
+ +# User-defined functions and program control flow + +## Writing programs with more than 1 line + +### Operator `;`, `then(...)` + +To effectively write programs that have more than one line, a programmer needs way to specify a sequence of commands +that execute one after another. In `scarpet` this can be achieved with `;`. Its an operator, and by separating +statements with semicolons. And since whitespaces and +`$` (commandline visible newline separator) +sign are all treats as whitespaces, how you layout your code doesn't matter, as long as it is readable to everyone involved. + +
+expr;
+expr;
+expr;
+expr
+
+ +Notice that the last expression is not followed by a semicolon. Since instruction separation is functional +in `scarpet`, and not barely an instruction delimiter, terminating the code with a dangling operator wouldn't +be valid. Having said that, since many programming languages don't care about the number of op terminators +programmers use, carpet preprocessor will remove all unnecessary semicolons from scripts when compiled. + +In general `expr; expr; expr; expr` is equivalent to `(((expr ; expr) ; expr) ; expr)` or `then(expr, expr, expr, expr)`. + +Result of the evaluated expression is the same as the result of the second expression, but first expression +is also evaluated for side-effects + +
+expr1 ; expr2 => expr2  // with expr1 as a side-effect
+
+ +## Global variables + +All defined functions are compiled, stored persistently, and available globally within the app. +Functions can only be undefined via call to `undef('fun')`, which would erase global entry for function `fun`. +Since all variables have local scope inside each function, or each command script, + global variables is a way to share the global state. + +Any variable that is used with a name that starts with `'global_'` will be stored and accessible globally, not only +inside the current scope. If used directly in the chat window with the default app, it will persist across calls to `/script` +function. Like functions, which are global, global variables can only be undefined via `undef`. + +For apps running in `'global'` scope - all players will share the same global variables and defined functions, +and with `player` scope, each player hosts its own state for each app, so function and global_variables are distinct. + + +
+/script run a() -> global_list+=1; global_list = [1,2,3]; a(); a(); global_list  // => [1, 2, 3, 1, 1]
+/script run a(); a(); global_list  // => [1, 2, 3, 1, 1, 1, 1]
+
+ +### `Operator ->` + +`->` operator has two uses - as a function definition operator and key-value initializer for maps. + +To organize code better than a flat sequence of operations, one can define functions. Definition is correct +if has the following form + +
+fun(args, ...) -> expr
+
+ +Where `fun(args, ...)` is a function signature indicating function name, number of arguments, and their names, +and expr is an expression (can be complex) that is evaluated when `fun` is called. Names in the signature don't +need to be used anywhere else, other occurrences of these names will be masked in this function scope. Function +call creates new scope for variables inside `expr`, so all non-global variables are not visible from the caller +scope. All parameters are passed by value to the new scope, including lists and other containers, however their +copy will be shallow. + +The function returns itself as a first class object, which means it can be used to call it later with the `call` function + +Using `_` as the function name creates anonymous function, so each time `_` function is defined, it will be given +a unique name, which you can pass somewhere else to get this function `call`ed. Anonymous functions can only be called +by their value and `call` method. + +
+a(lst) -> lst+=1; list = [1,2,3]; a(list); a(list); list  // => [1,2,3]
+
+ +In case the inner function wants to operate and modify larger objects, lists from the outer scope, but not global, +it needs to use `outer` function in function signature. + +in map construction context (directly in `m()` or `{}`), the `->` operator has a different function by converting its +arguments to a tuple which is used by map constructor as a key-value pair: + +
+{ 'foo' -> 'bar' } => {l('foo', 'bar')}
+
+ +This means that it is not possible to define literally a set of inline function, however a set of functions can still +be created by adding elements to an empty set, and building it this way. That's a tradeoff for having a cool map initializer. + +### `outer(arg)` + +`outer` function can only be used in the function signature, and it will cause an error everywhere else. It +saves the value of that variable from the outer scope and allows its use in the inner scope. This is a similar +behaviour to using outer variables in lambda function definitions from Java, except here you have to specify +which variables you want to use, and borrow + +This mechanism can be used to use static mutable objects without the need of using `global_...` variables + +
+list = [1,2,3]; a(outer(list)) -> list+=1;  a(); a(); list  // => [1,2,3,1,1]
+
+ +The return value of a function is the value of the last expression. This as the same effect as using outer or +global lists, but is more expensive + +
+a(lst) -> lst+=1; list = [1,2,3]; list=a(list); list=a(list); list  // => [1,2,3,1,1]
+
+ +Ability to combine more statements into one expression, with functions, passing parameters, and global and outer +scoping allow to organize even larger scripts + +### `Operator ...` + +Defines a function argument to represent a variable length argument list of whatever arguments are left +from the argument call list, also known as a varargs. There can be only one defined vararg argument in a function signature. +Technically, it doesn't matter where is it, but it looks best at the butt side of it. + +
+foo(a, b, c) -> ...  # fixed argument function, call foo(1, 2, 3)
+foo(a, b, ... c) -> ... # c is now representing the variable argument part
+    foo(1, 2)  # a:1, b:2, c:[]
+    foo(1, 2, 3)  # a:1, b:2, c:[3]
+    foo(1, 2, 3, 4)  # a:1, b:2, c:[3, 4] 
+foo(... x) -> ...  # all arguments for foo are included in the list
+
+    
+
+ +### `import(module_name, ? symbols ...)` + +Imports symbols from other apps and libraries into the current one: global variables or functions, allowing to use +them in the current app. This includes other symbols imported by these modules. Scarpet supports circular dependencies, +but if symbols are used directly in the module body rather than functions, it may not be able to retrieve them. + +Returns full list of available symbols that could be imported from this module, which can be used to debug import +issues, and list contents of libraries. + +You can load and import functions from dependencies in a remote app store's source specified in your config's `libraries` block, but make sure +to place your config _before_ the import in order to allow the remote dependency to be downloaded (currently, app resources are only downloaded +when using the `/carpet download` command). + +### `call(function, ? args ...)` + +calls a user defined function with specified arguments. It is equivalent to calling `function(args...)` directly +except you can use it with function value, or name instead. This means you can pass functions to other user defined +functions as arguments and call them with `call` internally. Since function definitions return the defined +function, they can be defined in place as anonymous functions. + +#### Passing function references to other modules of your application + +In case a function is defined by its name, Scarpet will attempt to resolve its definition in the given module and its imports, +meaning if the call is in a imported library, and not in the main module of your app, and that function is not visible from the +library perspective, but in the app, it won't be call-able. In case you pass a function name to a separate module in your app, +it should import back that method from the main module for visibility. + +Check an example of a problematic code of a library that expects a function value as a passed argument and how it is called in +the parent app: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme('foo' , 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` + +In this case `'foo'` will fail to dereference in `lib` as it is not visible by name. In tightly coupled modules, where `lib` is just +a component of your `app` you can use circular import to acknowledge the symbol from the other module (pretty much like +imports in Java classes), and that solves the issue but makes the library dependent on the main app: +``` +//lib.scl +import('app','foo'); +callme(fun, arg) -> call(fun, arg); +``` +You can circumvent that issue by explicitly dereferencing the local function where it is used as a lambda argument created +in the module in which the requested function is visible: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme(_(x) -> foo(x), 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` +Or by passing an explicit reference to the function, instead of calling it by name: +``` +//app.sc +import('lib', 'callme'); +global_foohandler = (foo(x) -> x*x); +test() -> callme(global_foohandler, 5); +``` + +Little technical note: the use of `_` in expression passed to built in functions is much more efficient due to not +creating new call stacks for each invoked function, but anonymous functions is the only mechanism available for +programmers with their own lambda arguments + +
+my_map(list, function) -> map(list, call(function, _));
+my_map([1,2,3], _(x) -> x*x);    // => [1,4,9]
+profile_expr(my_map([1,2,3], _(x) -> x*x));   // => ~32000
+sq(x) -> x*x; profile_expr(my_map([1,2,3], 'sq'));   // => ~36000
+sq = (_(x) -> x*x); profile_expr(my_map([1,2,3], sq));   // => ~36000
+profile_expr(map([1,2,3], _*_));   // => ~80000
+
+ +## Control flow + +### `return(expr?)` + +Sometimes its convenient to break the organized control flow, or it is not practical to pass the final result value of +a function to the last statement, in this case a return statement can be used + +If no argument is provided - returns null value. + +
+def() -> (
+   expr1;
+   expr2;
+   return(expr3); // function terminates returning expr3
+   expr4;     // skipped
+   expr5      // skipped
+)
+
+ +In general its cheaper to leave the last expression as a return value, rather than calling +returns everywhere, but it would often lead to a messy code. + +### `exit(expr?)` + +It terminates entire program passing `expr` as the result of the program execution, or null if omitted. + +### `try(expr)` `try(expr, user_catch_expr)` `try(expr, type, catch_expr, type?, catch_expr?, ...)` + +`try` evaluates expression, allowing capturing exceptions that would be thrown inside `expr` statement. The exceptions can be +thrown explicitly using `throw()` or internally by scarpet where code is correct but detects illegal state. The 2-argument form +catches only user-thrown exceptions and one argument call `try(expr)` is equivalent to `try(expr, null)`, +or `try(expr, 'user_exception', null)`. If multiple `type-catch` pairs are defined, the execution terminates on the first +applicable type for the exception thrown. Therefore, even if the caught exception matches multiple filters, only +the first matching block will be executed. + +Catch expressions are evaluated with +`_` set to the value associated with the exception and `_trace` set to contain details about point of error (token, and line and +column positions), call stack and local +variables at the time of failure. The `type` will catch any exception of that type and any subtype of this type. + + +You can use `try` mechanism to exit from large portion of a convoluted call stack and continue program execution, although catching +exceptions is typically much more expensive comparing to not throwing them. + +The `try` function allows you to catch some scarpet exceptions for cases covering invalid data, like invalid +blocks, biomes, dimensions and other things, that may have been modified by datapacks, resourcepacks or other mods, +or when an error is outside of the programmers scope, such as problems when reading or decoding files. + +This is the hierarchy of the exceptions that could be thrown/caught in the with the `try` function: +- `exception`: This is the base exception. Catching `'exception'` allows to catch everything that can be caught, +but like everywhere else, doing that sounds like a bad idea. + - `value_exception`: This is the parent for any exception that occurs due to an + incorrect argument value provided to a built-in function + - `unknown_item`, `unknown_block`, `unknown_biome`, `unknown_sound`, `unknown_particle`, + `unknown_poi_type`, `unknown_dimension`, `unknown_structure`, `unknown_criterion`: Specific + errors thrown when a specified internal name does not exist or is invalid. + - `io_exception`: This is the parent for any exception that occurs due to an error handling external data. + - `nbt_error`: Incorrect input/output NBT file. + - `json_error`: Incorrect input/output JSON data. + - `b64_error`: Incorrect input/output b64 (base 64) string + - `user_exception`: Exception thrown by default with `throw` function. + +Synopsis: +
+inner_call() ->
+(
+   aaa = 'booyah';
+   try(
+      for (range(10), item_tags('stick'+_*'k'));
+   ,
+      print(_trace) // not caught, only catching user_exceptions
+   )
+);
+
+outer_call() -> 
+( 
+   try(
+      inner_call()
+   , 'exception', // catching everything
+      print(_trace)
+   ) 
+);
+
+Producing: +``` +{stack: [[, inner_call, 1, 14]], locals: {_a: 0, aaa: booyah, _: 1, _y: 0, _i: 1, _x: 0, _z: 0}, token: [item_tags, 5, 23]} +``` + +### `throw(value?)`, `throw(type, value)`, `throw(subtype, type, value)` + +Throws an exception that can be caught in a `try` block (see above). If ran without arguments, it will throw a `user_exception` +passing `null` as the value to the `catch_expr`. With two arguments you can mimic any other exception type thrown in scarpet. +With 3 arguments, you can specify a custom exception acting as a `subtype` of a provided `type`, allowing to customize `try` +statements with custom exceptions. + +### `if(cond, expr, cond?, expr?, ..., default?)` + +If statement is a function that takes a number of conditions that are evaluated one after another and if any of +them turns out true, its `expr` gets returned, otherwise, if all conditions fail, the return value is `default` +expression, or `null` if default is skipped + +`if` function is equivalent to `if (cond) expr; else if (cond) expr; else default;` from Java, +just in a functional form +# Lists, Maps and API support for Containers + +Scarpet supports basic container types: lists and maps (aka hashmaps, dicts etc..) + +## Container manipulation + +Here is a list of operations that work on all types of containers: lists, maps, as well as other Minecraft specific +modifyable containers, like NBTs + +### `get(container, address, ...), get(lvalue), ':' operator` + +Returns the value at `address` element from the `value`. For lists it indicates an index, use negative numbers to +reach elements from the end of the list. `get` call will always be able to find the index. In case there is few +items, it will loop over + +for maps, retrieves the value under the key specified in the `address` or null otherwise + +[Minecraft specific usecase]: In case `value` is of `nbt` type, uses address as the nbt path to query, returning null, +if path is not found, one value if there was one match, or list of values if result is a list. Returned elements can +be of numerical type, string texts, or another compound nbt tags + +In case to simplify the access with nested objects, you can add chain of addresses to the arguments of `get` rather +than calling it multiple times. `get(get(foo,a),b)` is equivalent to `get(foo, a, b)`, or `foo:a:b`. + +
+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({ 'foo' -> 2, 'bar' -> 3, 'baz' -> 4 }, 'bar')  => 3
+
+ +### `has(container, address, ...), has(lvalue)` + +Similar to `get`, but returns boolean value indicating if the given index / key / path is in the container. +Can be used to determine if `get(...)==null` means the element doesn't exist, or the stored value for this +address is `null`, and is cheaper to run than `get`. + +Like get, it can accept multiple addresses for chains in nested containers. In this case `has(foo:a:b)` is +equivalent to `has(get(foo,a), b)` or `has(foo, a, b)` + +### `delete(container, address, ...), delete(lvalue)` + +Removes specific entry from the container. For the lists - removes the element and shrinks it. For maps, it +removes the key from the map, and for nbt - removes content from a given path. + +Like with the `get` and `has`, `delete` can accept chained addresses, as well as l-value container access, removing +the value from the leaf of the path provided, so `delete(foo, a, b)` is the +same as `delete(get(foo,a),b)` or `delete(foo:a:b)` + +Returns true, if container was changed, false, if it was left unchanged, and null if operation was invalid. + +### `put(container, address, value), put(container, address, value, mode), put(lvalue, value)` + +**Lists** + +Modifies the container by replacing the value under the address with the supplied `value`. For lists, a valid +index is required, but can be negative as well to indicate positions from the end of the list. If `null` is +supplied as the address, it always means - add to the end of the list. + +There are three modes that lists can have items added to them: + +* `replace`(default): Replaces item under given index(address). Doesn't change the size of the array +unless `null` address is used, which is an exception and then it appends to the end +* `insert`: Inserts given element at a specified index, shifting the rest of the array to make space for the item. +Note that index of -1 points to the last element of the list, thus inserting at that position and moving the previous +last element to the new last element position. To insert at the end, use `+=` operator, or `null` address in put +* `extend`: treats the supplied value as an iterable set of values to insert at a given index, extending the list +by this amount of items. Again use `null` address/index to point to the end of the list + +Due to the extra mode parameter, there is no chaining for `put`, but you can still use l-value container access to +indicate container and address, so `put(foo, key, value)` is the same as `put(foo:key, value)` or `foo:key=value` + +Returns true, if container got modified, false otherwise, and null if operation was invalid. + +**Maps** + +For maps there are no modes available (yet, seems there is no reason to). It replaces the value under the supplied +key (address), or sets it if not currently present. + +**NBT Tags** + +The address for nbt values is a valid nbt path that you would use with `/data` command, and tag is any tag that +would be applicable for a given insert operation. Note that to distinguish between proper types (like integer types, +you need to use command notation, i.e. regular ints is `123`, while byte size int would be `123b` and an explicit +string would be `"5"`, so it helps that scarpet uses single quotes in his strings. Unlike for lists and maps, it +returns the number of affected nodes, or 0 if none were affected. + +There are three modes that NBT tags can have items added to them: + +* `replace`(default): Replaces item under given path(address). Removes them first if possible, and then adds given +element to the supplied position. The target path can indicate compound tag keys, lists, or individual elements +of the lists. +* ``: Index for list insertions. Inserts given element at a specified index, inside a list specified with the +path address. Fails if list is not specified. It behaves like `insert` mode for lists, i.e. it is not removing any +of the existing elements. Use `replace` to remove and replace existing element. +* `merge`: assumes that both path and replacement target are of compound type (dictionaries, maps, `{}` types), +and merges keys from `value` with the compound tag under the path + +
+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
+     => {a:[{lvl:[1,2,1,3]},{lvl:[3,2,1,1]},{lvl:[4,5,1,6]}]}
+tag = nbt('{a:[{lvl:[1,2,3]},{lvl:[3,2,1]},{lvl:[4,5,6]}]}'); put(tag, 'a[].lvl[1]', 1); tag
+     => {a:[{lvl:[1,1,3]},{lvl:[3,1,1]},{lvl:[4,1,6]}]}
+
+ +## List operations + +### `[value, ...?]`,`[iterator]`,`l(value, ...?)`, `l(iterator)` + +Creates a list of values of the expressions passed as parameters. It can be used as an L-value and if all +elements are variables, you coujld use it to return multiple results from one function call, if that +function returns a list of results with the same size as the `[]` call uses. In case there is only one +argument and it is an iterator (vanilla expression specification has `range`, but Minecraft API implements +a bunch of them, like `diamond`), it will convert it to a proper list. Iterators can only be used in high order +functions, and are treated as empty lists, unless unrolled with `[]`. + +Internally, `[elem, ...]`(list syntax) and `l(elem, ...)`(function syntax) are equivalent. `[]` is simply translated to +`l()` in the scarpet preprocessing stage. This means that internally the code has always expression syntax despite `[]` +not using different kinds of brackets and not being proper operators. This means that `l(]` and `[)` are also valid +although not recommended as they will make your code far less readable. + +
+l(1,2,'foo') <=> [1, 2, 'foo']
+l() <=> [] (empty list)
+[range(10)] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+[1, 2] = [3, 4] => Error: l is not a variable
+[foo, bar] = [3, 4]; foo==3 && bar==4 => 1
+[foo, bar, baz] = [2, 4, 6]; [min(foo, bar), baz] = [3, 5]; [foo, bar, baz]  => [3, 4, 5]
+
+ +In the last example `[min(foo, bar), baz]` creates a valid L-value, as `min(foo, bar)` finds the lower of the +variables (in this case `foo`) creating a valid assignable L-list of `[foo, baz]`, and these values +will be assigned new values + +### `join(delim, list), join(delim, values ...)` + +Returns a string that contains joined elements of the list, iterator, or all values, +concatenated with `delim` delimiter + +
+join('-',range(10))  => 0-1-2-3-4-5-6-7-8-9
+join('-','foo')  => foo
+join('-', 'foo', 'bar')  => foo-bar
+
+ +### `split(delim?, expr)` + +Splits a string under `expr` by `delim` which can be a regular expression. If no delimiter is specified, it splits +by characters. + +If `expr` is a list, it will split the list into multiple sublists by the element (s) which equal `delim`, or which equal the empty string +in case no delimiter is specified. + +Splitting a `null` value will return an empty list. + +
+split('foo') => [f, o, o]
+split('','foo')  => [f, o, o]
+split('.','foo.bar')  => []
+split('\\.','foo.bar')  => [foo, bar]
+split(1,[2,5,1,2,3,1,5,6]) => [[2,5],[2,3],[5,6]]
+split(1,[1,2,3,1,4,5,1] => [[], [2,3], [4,5], []]
+split(null) => []
+
+ +### `slice(expr, from, to?)` + +extracts a substring, or sublist (based on the type of the result of the expression under expr with +starting index of `from`, and ending at `to` if provided, or the end, if omitted. Can use negative indices to +indicate counting form the back of the list, so `-1 <=> length(_)`. + +Special case is made for iterators (`range`, `rect` etc), which does require non-negative indices (negative `from` is treated as +`0`, and negative `to` as `inf`), but allows retrieving parts of the sequence and ignore +other parts. In that case consecutive calls to `slice` will refer to index `0` the current iteration position since iterators +cannot go back nor track where they are in the sequence (see examples). + +
+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]
+slice(range(10), 5)  => [5, 6, 7, 8, 9]
+r = range(100); [slice(r, 5, 7), slice(r, 1, 3)]  => [[5, 6], [8, 9]]
+
+ +### `sort(list), sort(values ...)` + +Sorts in the default sortographical order either all arguments, or a list if its the only argument. +It returns a new sorted list, not affecting the list passed to the argument + +
sort(3,2,1)  => [1, 2, 3]
+sort('a',3,11,1)  => [1, 3, 11, 'a']
+list = [4,3,2,1]; sort(list)  => [1, 2, 3, 4]
+
+ +### `sort_key(list, key_expr)` + +Sorts a copy of the list in the order or keys as defined by the `key_expr` for each element + +
+sort_key([1,3,2],_)  => [1, 2, 3]
+sort_key([1,3,2],-_)  => [3, 2, 1]
+sort_key([range(10)],rand(1))  => [1, 0, 9, 6, 8, 2, 4, 5, 7, 3]
+sort_key([range(20)],str(_))  => [0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9]
+
+ +### `range(to), range(from, to), range(from, to, step)` + +Creates a range of numbers from `from`, no greater/larger than `to`. The `step` parameter dictates not only the +increment size, but also direction (can be negative). The returned value is not a proper list, just the iterator +but if for whatever reason you need a proper list with all items evaluated, use `[range(to)]`. +Primarily to be used in higher order functions + +
+range(10)  => [...]
+[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]
+range(20, 10, -2)  => [20, 18, 16, 14, 12]
+range(-0.3, 0.3, 0.1)  => [-0.3, -0.2, -0.1, 0, 0.1, 0.2]
+range(0.3, -0.3, -0.1) => [0.3, 0.2, 0.1, -0, -0.1, -0.2]
+
+ +## Map operations + +Scarpet supports map structures, aka hashmaps, dicts etc. Map structure can also be used, with `null` values as sets. +Apart from container access functions, (`. , get, put, has, delete`), the following functions: + +### `{values, ...}`,`{iterator}`,`{key -> value, ...}`,`m(values, ...)`, `m(iterator)`, `m(l(key, value), ...))` + +creates and initializes a map with supplied keys, and values. If the arguments contains a flat list, these are all +treated as keys with no value, same goes with the iterator - creates a map that behaves like a set. If the +arguments is a list of lists, they have to have two elements each, and then first is a key, and second, a value + +In map creation context (directly inside `{}` or `m{}` call), `->` operator acts like a pair constructor for simpler +syntax providing key value pairs, so the invocation to `{foo -> bar, baz -> quux}` is equivalent to +`{[foo, bar], [baz, quux]}`, which is equivalent to somewhat older, but more traditional functional form of +`m(l(foo, bar),l(baz, quuz))`. + +Internally, `{?}`(list syntax) and `m(?)`(function syntax) are equivalent. `{}` is simply translated to +`m()` in the scarpet preprocessing stage. This means that internally the code has always expression syntax despite `{}` +not using different kinds of brackets and not being proper operators. This means that `m(}` and `{)` are also valid +although not recommended as they will make your code far less readable. + +When converting map value to string, `':'` is used as a key-value separator due to tentative compatibility with NBT +notation, meaning in simpler cases maps can be converted to NBT parsable string by calling `str()`. This however +does not guarantee a parsable output. To properly convert to NBT value, use `encode_nbt()`. + +
+{1, 2, 'foo'} => {1: null, 2: null, foo: null}
+m() <=> {} (empty map)
+{range(10)} => {0: null, 1: null, 2: null, 3: null, 4: null, 5: null, 6: null, 7: null, 8: null, 9: null}
+m(l(1, 2), l(3, 4)) <=> {1 -> 2, 3 -> 4} => {1: 2, 3: 4}
+reduce(range(10), put(_a, _, _*_); _a, {})
+     => {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
+
+ +### `keys(map), values(map), pairs(map)` + +Returns full lists of keys, values and key-value pairs (2-element lists) for all the entries in the map +# Minecraft specific API and `scarpet` language add-ons and commands + +Here is the gist of the Minecraft related functions. Otherwise the scarpet could live without Minecraft. + +## Global scarpet options + +These options affect directly how scarpet functions and can be triggered via `/carpet` command. + - `commandScript`: disables `/script` command making it impossible to control apps in game. Apps will still load and run + when loaded with the world (i.e. present in the world/scripts folder) + - `scriptsAutoload`: when set to `false` will prevent apps loaded with the world to load automatically. You can still + load them on demand via `/script load` command + - `commandScriptACE`: command permission level that is used to trigger commands from scarpet scripts (regardless who triggers + the code that calls the command). Defaults to `ops`, could be customized to any level via a numerical value (0, 1, 2, 3 or 4) + - `scriptsOptimization`: when disabled, disables default app compile time optimizations. If your app behaves differently with + and without optimizations, please file a bug report on the bug tracker and disable code optimizations. + - `scriptsDebugging`: Puts detailed information about apps loading, performance and runtime in system log. + - `scriptsAppStore`: location of the app store for downloadable scarpet apps - can be configured to point to other scarpet app store. + +## App structure + +The main delivery method for scarpet programs into the game is in the form of apps in `*.sc` files located in the world `scripts` +folder, flat. In singleplayer, you can also save apps in `.minecraft/config/carpet/scripts` for them to be available in any world, +and here you can actually organize them in folders. +When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope +(more about the app scopes below), without executing of any functions, unless called directly, and with the exception of the +`__config()` function, if present, which will be executed once. Loading the app will also bind specific +events to the event system (check Events section for details). + +If an app defines `__on_start()` function, it will be executed once before running anything else. For global scoped apps, +this is just after they are loaded, and for player scoped apps, before they are used first time by a player. +Unlike static code (written directly in the body of the app code), that always run once per app, this may run multiple times if +its a player app nd multiple players are on the server. + +Unloading an app removes all of its state from the game, disables commands, removes bounded events, and +saves its global state. If more cleanup is needed, one can define `__on_close()` function which will be +executed when the module is unloaded, or server is closing or crashing. However, there is no need to do that +explicitly for the things that clean up automatically, as indicated in the previous statement. With `'global'` scoped +apps `__on_close()` will execute once per app, and with `'player'` scoped apps, will execute once per player per app. + +### App config via `__config()` function + +If an app defines `__config` method, and that method returns a map, it will be used to apply custom settings +for this app. Currently, the following options are supported: + +* `'strict'` : if `true`, any use of an uninitialized variable will result in program failure. Defaults to `false` if +not specified. With `'strict'`you have to assign an initial value to any variable before using it. It is very useful +to use this setting for app debugging and for beginner programmers. Explicit initialization is not required for your +code to work, but mistakes may result from improper assumptions about initial variable values of `null`. +* `'scope'`: default scope for global variables for the app, Default is `'player'`, which means that globals and defined +functions will be unique for each player so that apps for each player will run in isolation. This is useful in +tool-like applications, where behaviour of things is always from a player's perspective. With player scope the initial run +of the app creates is initial state: defined functions, global variables, config and event handlers, which is then copied for +each player that interacts with the app. With `'global'` scope - the state created by the initial load is the only variant of +the app state and all players interactions run in the same context, sharing defined functions, globals, config and events. +`'global'` scope is most applicable to world-focused apps, where either players are not relevant, or player data is stored +explicitly keyed with players, player names, uuids, etc. +Even for `'player'` scoped apps, you can access specific player app with with commandblocks using +`/execute as run script in run ...`. +To access global/server state for a player app, which you shouldn't do, you need to disown the command from any player, +so either use a command block, or any +arbitrary entity: `/execute as @e[type=bat,limit=1] run script in globals` for instance, however +running anything in the global scope for a `'player'` scoped app is not intended. +* `'event_priority'`: defaults to `0`. This specifies the order in which events will be run, from highest to lowest. +This is need since cancelling an event will stop executing the event in subsequent apps with lower priority. +* `'stay_loaded'`: defaults to `true`. If true, and `/carpet scriptsAutoload` is turned on, the following apps will +stay loaded after startup. Otherwise, after reading the app the first time, and fetching the config, server will drop them down. + WARNING: all apps will run once at startup anyways, so be aware that their actions that are called +statically, will be performed once anyways. Only apps present in the world's `scripts` folder will be autoloaded. +* `'legacy_command_type_support'` - if `true`, and the app defines the legacy command system via `__command()` function, +all parameters of command functions will be interpreted and used using brigadier / vanilla style argument parser and their type +will be inferred from their names, otherwise +the legacy scarpet variable parser will be used to provide arguments to commands. +* `'allow_command_conflicts'` - if custom app commands tree is defined, the app engine will check and identify +conflicts and ambiguities between different paths of execution. While ambiguous commands are allowed in brigadier, +and they tend to execute correctly, the suggestion support works really poorly in these situations and scarpet +will warn and prevent such apps from loading with an error message. If `allow_command_conflicts` is specified and +`true`, then scarpet will load all provided commands regardless. +* `'requires'` - defines either a map of mod dependencies in Fabric's mod.json style, or a function to be executed. If it's a map, it will only + allow the app to load if all of the mods specified in the map meet the version criteria. If it's a function, it will prevent the app from + loading if the function does not execute to `false`, displaying whatever is returned to the user. + + Available prefixes for the version comparison are `>=`, `<=`, `>`, `<`, `~`, `^` and `=` (default if none specified), based in the spec + at [NPM docs about SemVer ranges](https://docs.npmjs.com/cli/v6/using-npm/semver#ranges) + ``` + __config() -> { + 'requires' -> { + 'carpet' -> '>=1.4.33', // Will require Carpet with a version >= 1.4.32 + 'minecraft' -> '>=1.16', // Will require Minecraft with a version >= 1.16 + 'chat-up' -> '*' // Will require any version of the chat-up mod + } + } + ``` + ``` + __config() -> { + 'requires' -> _() -> ( + d = convert_date(unix_time()); + if(d:6 == 5 && d:2 == 13, + 'Its Friday, 13th' // Will throw this if Friday 13th, will load else since `if` function returns `null` by default + ) + } + ``` +* `'command_permission'` - indicates a custom permission to run the command. It can either be a number indicating +permission level (from 1 to 4) or a string value, one of: `'all'` (default), `'ops'` (default opped player with permission level of 2), +`'server'` - command accessible only through the server console and commandblocks, but not in chat, `'players'` - opposite +of the former, allowing only use in player chat. It can also be a function (lambda function or function value, not function name) +that takes 1 parameter, which represents the calling player, or `'null'` if the command represents a server call. +The function will prevent the command from running if it evaluates to `false`. +Please note, that Minecraft evaluates eligible commands for players when they join, or on reload/restart, so if you use a +predicate that is volatile and might change, the command might falsely do or do not indicate that it is available to the player, +however player will always be able to type it in and either succeed, or fail, based on their current permissions. +Custom permission applies to legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below. +* `'resources'` - list of all downloadable resources when installing the app from an app store. List of resources needs to be +in a list and contain of map-like resources descriptors, looking like + ``` + 'resources' -> [ + { + 'source' -> 'https://raw.githubusercontent.com/gnembon/fabric-carpet/master/src/main/resources/assets/carpet/icon.png', + 'target' -> 'foo/photos.zip/foo/cm.png', + }, + { + 'source' -> '/survival/README.md', + 'target' -> 'survival_readme.md', + 'shared' -> true, + }, + { + 'source' -> 'circle.sc', // Relative path + 'target' -> 'apps/circle.sc', // This won't install the app, use 'libraries' for that + }, + ] + ``` + `source` indicates resource location: either an arbitrary url (starting with `http://` or `https://`), + absolute location of a file in the app store (starting with a slash `/`), +or a relative location in the same folder as the app in question (the relative location directly). +`'target'` points to the path in app data, or shared app data folder. If not specified it will place the app into the main data folder with the name it has. +if `'shared'` is specified and `true`. When re-downloading the app, all resources will be re-downloaded as well. +Currently, app resources are only downloaded when using `/script download` command. +* `libraries` - list of libraries or apps to be downloaded when installing the app from the app store. It needs to be a list of map-like resource +descriptors, like the above `resources` field. + ``` + 'libraries' -> [ + { + 'source' -> '/tutorial/carpets.sc' + }, + { + 'source' -> '/fundamentals/heap.sc', + 'target' -> 'heap-lib.sc' + } + ] + ``` + `source` indicates resource location and must point to a scarpet app or library. It can be either an arbitrary url (starting with `http://` + or `https://`), absolute location of a file in the app store (starting with a slash `/`), or a relative location in the same folder as the app + in question (the relative location directly). + `target` is an optional field indicating the new name of the app. If not specified it will place the app into the main data folder with the name it has. +If the app has relative resources dependencies, Carpet will use the app's path for relatives if the app was loaded from the same app store, or none if the +app was loaded from an external url. +If you need to `import()` from dependencies indicated in this block, make sure to have the `__config()` map before any import that references your +remote dependencies, in order to allow them to be downloaded and initialized before the import is executed. +* `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below. +* `'commands'` - defines custom commands for the app to be executed with `/` command, see below. + +## Custom app commands + +Apps can register custom commands added to the existing command system under `/` where `` is the +name of the app. There are three ways apps can provide commands: + +### Simple commands without custom argument support + +Synopsis: +``` +__command() -> 'root command' +foo() -> 'running foo'; +bar(a, b) -> a + b; +baz(a, b) -> // same thing +( + print(a+b); + null +) +``` + +If a loaded app contains `__command()` method, it will attempt to register a command with that app name, +and register all public (not starting with an underscore) functions available in the app as subcommands, in the form of +`/ `. Arguments are parsed from a single +`greedy string` brigadier argument, and split into function parameters. Parsing of arguments is limited +to numbers, string constants, and available global variables, whitespace separated. Using functions and operators other than +unary `-`, would be unsafe, so it is not allowed. +In this mode, if a function returns a non-null value, it will be printed as a result to the +invoker (e.g. in chat). If the provided argument list does not match the expected argument count of a function, an error message +will be generated. + +Running the app command that doesn't take any extra arguments, so `/` will run the `__command() -> ` function. + +This mode is best for quick apps that typically don't require any arguments and want to expose some functionality in a +simple and convenient way. + +### Simple commands with vanilla argument type support + +Synopsis: + ``` +__config() -> {'legacy_command_type_support' -> true}; +__command() -> print('root command'); +foo() -> print('running foo'); +add(first_float, other_float) -> print('sum: '+(first_float+other_float)); +bar(entitytype, item) -> print(entitytype+' likes '+item:0); +baz(entities) -> // same thing + ( + print(join(',',entities)); + ) + ``` + +It works similarly to the auto command, but arguments get their inferred types based on the argument +names, looking at the full name, or any suffix when splitting on `_` that indicates the variable type. For instance, variable named `float` will +be parsed as a floating point number, but it can be named `'first_float'` or `'other_float'` as well. Any variable that is not +supported, will be parsed as a `'string'` type. + +Argument type support includes full support for custom argument types (see below). + +### Custom commands + +Synopsis. This example mimics vanilla `'effect'` command adding extra parameter that is not +available in vanilla - to optionally hide effect icon from UI: +``` +global_instant_effects = {'instant_health', 'instant_damage', 'saturation'}; +__config() -> { + 'commands' -> + { + '' -> _() -> print('this is a root call, does nothing. Just for show'), + 'clear' -> _() -> clear_all([player()]), + 'clear ' -> 'clear_all', + 'clear ' -> 'clear', + 'give ' -> ['apply', -1, 0, false, true], + 'give ' -> ['apply', 0, false, true], + 'give ' -> ['apply', false, true], + 'give ' -> ['apply', true], + 'give ' -> 'apply', + + }, + 'arguments' -> { + 'seconds' -> { 'type' -> 'int', 'min' -> 1, 'max' -> 1000000, 'suggest' -> [60]}, + 'amplifier' -> { 'type' -> 'int', 'min' -> 0, 'max' -> 255, 'suggest' -> [0]}, + 'hideParticles' -> {'type' -> 'bool'}, // pure rename + 'showIcon' -> {'type' -> 'bool'}, // pure rename + } +}; + + +clear_all(targets) -> for(targets, modify(_, 'effect')); +clear(targets, effect) -> for(targets, modify(_, 'effect', effect)); +apply(targets, effect, seconds, amplifier, part, icon) -> +( + ticks = if (has(global_instant_effects, effect), + if (seconds < 0, 1, seconds), + if (seconds < 0, 600, 20*seconds) + ); + for (targets, modify(_, 'effect', effect, ticks, amplifier, part, icon)); +) +``` + +This is the most flexible way to specify custom commands with scarpet. it works by providing command +paths with functions to execute, and optionally, custom argument types. Commands are listed in a map, where +the key (can be empty) consists of +the execution path with the command syntax, which consists of literals (as is) and arguments (wrapped with `<>`), with the name / suffix +of the name of the attribute indicating its type, and the value represent function to call, either function values, +defined function names, or functions with some default arguments. Argument names need to be unique for each command. Values extracted from commands will be passed to the +functions and executed. By default, command list will be checked for ambiguities (commands with the same path up to some point +that further use different attributes), causing app loading error if that happens, however this can be suppressed by specifying +`'allow_command_conflicts'`. + +Unlike with legacy command system with types support, names of the arguments and names of the function parameters don't need to match. +The only important aspect is the argument count and argument order. + +Custom commands provide a substantial subset of brigadier features in a simple package, skipping purposely on some less common +and less frequently used features, like forks and redirects, used pretty much only in the vanilla `execute` command. + +### Command argument types + +Argument types differ from actual argument names that the types are the suffixes of the used argument names, when separated with +`'_'` symbol. For example argument name `'from_pos'` will be interpreted as a built-in type `'int'` and provided to the command system +as a name `'from_pos'`, however if you define a custom type `'from_pos'`, your custom type will be used instead. +Longer suffixes take priority over shorter prefixes, then user defined suffixes mask build-in prefixes. + +There are several default argument types that can be used directly without specifying custom types. + +Each argument can be customized in the `'arguments'` section of the app config, specifying its base type, via `'type'` that needs +to match any of the built-in types, with a series of optional modifiers. Shared modifiers include: + * `suggest` - static list of suggestions to show above the command while typing + * `suggester` - function taking one map argument, indicating current state of attributes in the parsed command + suggesting a dynamic list of valid suggestions for typing. For instance here is a term based type matching + all loaded players adding Steve and Alex, and since player list changes over time cannot be provided statically: + ``` +__config() -> { + 'arguments' -> { + 'loadedplayer' -> { + 'type' -> 'term', + 'suggester' -> _(args) -> ( + nameset = {'Steve', 'Alex'}; + for(player('all'), nameset += _); + keys(nameset) + ), + } + } +}; + ``` + * `case_sensitive` - whether suggestions are case sensitive, defaults to true + +Here is a list of built-in types, with their return value formats, as well as a list of modifiers + that can be customized for that type (if any) + * `'string'`: a string that can be quoted to include spaces. Customizable with `'options'` - a + static list of valid options it can take. command will fail if the typed string is not in this list. + * `'term'`: single word string, no spaces. Can also be customized with `'options'` + * `'text'`: the rest of the command as a string. Has to be the last argument. Can also be customized with `'options'` + * `'bool'`: `true` or `false` + * `'float'`: a number. Customizable with `'min'` and `'max'` values. + * `'int'`: a number, requiring an integer value. Customizable with `'min'` and `'max'` values. + * `'yaw'`: a number, requiring a valid yaw angle. + * `'pos'`: block position as a triple of coordinates. Customized with `'loaded'`, if true requiring the position + to be loaded. + * `'block'`: a valid block state wrapped in a block value (including block properties and data) + * `'blockpredicate`': returns a 4-tuple indicating conditions of a block to match: block name, block tag, + map of required state properties, and tag to match. Either block name or block tag are `null` but not both. + Property map is always specified, but its empty for no conditions, and matching nbt tag can be `null` indicating + no requirements. Technically the 'all-matching' predicate would be `[null, null, {}, null]`, but + block name or block tag is always specified. One can use the following routine to match a block agains this predicate: + ``` + block_to_match = block(x,y,z); + [block_name, block_tag, properties, nbt_tag] = block_predicate; + + (block_name == null || block_name == block_to_match) && + (block_tag == null || block_tags(block_to_match, block_tag)) && + all(properties, block_state(block_to_match, _) == properties:_) && + (!tag || tag_matches(block_data(block_to_match), tag)) + ``` + * `'teamcolor'`: name of a team, and an integer color value of one of 16 valid team colors. + * `'columnpos'`: a pair of x and z coordinates. + * `'dimension'`: string representing a valid dimension in the world. + * `'anchor'`: string of `feet` or `eyes`. + * `'entitytype'`: string representing a type of entity + * `'entities'`: entity selector, returns a list of entities directly. Can be configured with `'single'` to only accept a single entity (will return the entity instead of a singleton) and with `'players'` to only accept players. + * `'floatrange'`: pair of two numbers where one is smaller than the other + * `'players'`: returning a list of valid player name string, logged in or not. If configured with `'single'` returns only one player or `null`. + * `'intrange'`: same as `'floatrange'`, but requiring integers. + * `'enchantment'`: name of an enchantment + * `'slot'`: provides a list of inventory type and slot. Can be configured with `'restrict'` requiring + `'player'`, `'enderchest'`, `'equipment'`, `'armor'`, `'weapon'`, `'container'`, `'villager'` or `'horse'` restricting selection of + available slots. Scarpet supports all vanilla slots, except for `horse.chest` - chest item, not items themselves. This you would + need to manage yourself via nbt directly. Also, for entities that change their capacity, like llamas, you need to check yourself if + the specified container slot is valid for your entity. + * `'item'`: triple of item type, count of 1, and nbt. + * `'message'`: text with expanded embedded player names + * `'effect'`: string representing a status effect + * `'path'`: a valid nbt path + * `'objective'`: a tuple of scoreboard objective name and its criterion + * `'criterion'`: name of a scoreboard criterion + * `'particle'`: name of a particle + * `'recipe'`: name of a valid recipe. Can be fed to recipe_data function. + * `'advancement'`: name of an advancement + * `'lootcondition'`: a loot condition + * `'loottable'`: name of a loot table source + * `'attribute'`: an attribute name + * `'boss'`: a bossbar name + * `'biome'`: a biome name. or biome tag + * `'sound'`: name of a sound + * `'storekey'`: string of a valid current data store key. + * `'identifier'`: any valid identifier. 'minecraft:' prefix is stripped off as a default. + Configurable with `'options'` parameter providing a static list of valid identifiers. + * `'rotation'`: pair of two numbers indicating yaw and pitch values. + * `'scoreholder'`: list of strings of valid score holders. Customizable with `'single'` that makes the parameter require a single target, retuning `null` if its missing + * `'scoreboardslot'` string representing a valid location of scoreboard display. + * `'swizzle'` - set of axis as a string, sorted. + * `'time'` - number of ticks representing a duration of time. + * `'uuid'` - string of a valid uuid. + * `'surfacelocation'` - pair of x and z coordinates, floating point numbers. + * `'location'` - triple of x, y and z coordinates, optionally centered on the block if + interger coordinates are provided and `'block_centered'` optional modifier is `true`. + +## Dimension warning + +One note, which is important is that most of the calls for entities and blocks would refer to the current +dimension of the caller, meaning, that if we for example list all the players using `player('all')` function, +if a player is in the other dimension, calls to entities and blocks around that player would be incorrect. +Moreover, running commandblocks in the spawn chunks would mean that commands will always refer to the overworld +blocks and entities. In case you would want to run commands across all dimensions, just run three of them, +using `/execute in overworld/the_nether/the_end run script run ...` and query players using `player('*')`, +which only returns players in current dimension, or use `in_dimension(expr)` function. +# Blocks / World API + +## Specifying blocks + +### `block(x, y, z)`, `block([x,y,z])`, `block(state)` + +Returns either a block from specified location, or block with a specific state (as used by `/setblock` command), +so allowing for block properties, block entity data etc. Blocks otherwise can be referenced everywhere by its simple +string name, but its only used in its default state. + +
+block('air')  => air
+block('iron_trapdoor[half=top]')  => iron_trapdoor
+block(0,0,0) == block('bedrock')  => 1
+block('hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') => hopper
+
+ +Retrieving a block with `block` function has also a side-effect of evaluating its current state and data. +So if you use it later it will reflect block state and data of the block that was when block was called, rather than +when it was used. Block values passed in various places like `scan` functions, etc, are not fully evaluated unless +its properties are needed. This means that if the block at the location changes before its queried in the program this +might result in getting the later state, which might not be desired. Consider the following example: + +Throws `unknown_block` if provided input is not valid. + +
set(10,10,10,'stone');
+scan(10,10,10,0,0,0, b = _);
+set(10,10,10,'air');
+print(b); // 'air', block was remembered 'lazily', and evaluated by `print`, when it was already set to air
+set(10,10,10,'stone');
+scan(10,10,10,0,0,0, b = block(_));
+set(10,10,10,'air');
+print(b); // 'stone', block was evaluated 'eagerly' but call to `block`
+
+ +## World Manipulation + +All the functions below can be used with block value, queried with coord triple, or 3-long list. All `pos` in the +functions referenced below refer to either method of passing block position. + +### `set(pos, block, property?, value?, ..., block_data?)`, `set(pos, block, [property?, value?, ...], block_data?)`, `set(pos, block, {property? -> value?, ...}, block_data?)` + +First argument for the `set` function is either a coord triple, list of three numbers, or a world localized block value. +Second argument, `block`, is either an existing block value, a result of `block()` function, or a string value indicating the block name +with optional state and block data. It is then followed by an optional +`property - value` pairs for extra block state (which can also be provided in a list or a map). Optional `block_data` include the block data to +be set for the target block. + +If `block` is specified only by name, then if a +destination block is the same the `set` operation is skipped, otherwise is executed, for other potential extra +properties that the original source block may have contained. + +The returned value is either the block state that has been set, or `false` if block setting was skipped, or failed + +Throws `unknown_block` if provided block to set is not valid + +
+set(0,5,0,'bedrock')  => bedrock
+set([0,5,0], 'bedrock')  => bedrock
+set(block(0,5,0), 'bedrock')  => bedrock
+scan(0,5,0,0,0,0,set(_,'bedrock'))  => 1
+set(pos(player()), 'bedrock')  => bedrock
+set(0,0,0,'bedrock')  => 0   // or 1 in overworlds generated in 1.8 and before
+scan(0,100,0,20,20,20,set(_,'glass'))
+    // filling the area with glass
+scan(0,100,0,20,20,20,set(_,block('glass')))
+    // little bit faster due to internal caching of block state selectors
+b = block('glass'); scan(0,100,0,20,20,20,set(_,b))
+    // yet another option, skips all parsing
+set(x,y,z,'iron_trapdoor')  // sets bottom iron trapdoor
+
+set(x,y,z,'iron_trapdoor[half=top]')  // sets the top trapdoor
+set(x,y,z,'iron_trapdoor','half','top') // also correct - top trapdoor
+set(x,y,z,'iron_trapdoor', ['half','top']) // same
+set(x,y,z,'iron_trapdoor', {'half' -> 'top'}) // same
+set(x,y,z, block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing of block state
+
+set(x,y,z,'hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') // extra block data
+set(x,y,z,'hopper', {'facing' -> 'north'}, nbt('{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') ) // same
+
+ +### `without_updates(expr)` + +Evaluates subexpression without causing updates when blocks change in the world. + +For synchronization sake, as well as from the fact that suppressed update can only happen within a tick, +the call to the `expr` is docked on the main server task. + +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: + +
+__config() -> {'scope' -> 'global'};
+__on_chunk_generated(x, z) -> (
+  scan(x,0,z,0,0,0,15,15,15,
+    if (perlin(_x/16, _y/8, _z/16) > _y/16,
+      set(_, 'black_stained_glass');
+    )
+  )
+)
+
+ +The following addition resolves this issue, by not allowing block updates past chunk borders: + +
+__config() -> {'scope' -> 'global'};
+__on_chunk_generated(x, z) -> (
+  without_updates(
+    scan(x,0,z,0,0,0,15,15,15,
+      if (perlin(_x/16, _y/8, _z/16) > _y/16,
+        set(_, 'black_stained_glass');
+      )
+    )
+  )
+)
+
+ +### `place_item(item, pos, facing?, sneak?)` + +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. + +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. + +Throws `unknown_item` if `item` doesn't exist + +
+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?)` + +Sets a Point of Interest (POI) of a specified type with optional custom occupancy. By default new POIs are not occupied. +If type is `null`, POI at position is removed. In any case, previous POI is also removed. Available POI types are: + +* `'unemployed', 'armorer', 'butcher', 'cartographer', 'cleric', 'farmer', 'fisherman', 'fletcher', 'leatherworker', 'librarian', 'mason', 'nitwit', 'shepherd', 'toolsmith', 'weaponsmith', 'home', 'meeting', 'beehive', 'bee_nest', 'nether_portal'` + +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. + +Throws `unknown_poi` if the provided point of interest doesn't exist + +### `set_biome(pos, biome_name, update=true)` + +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 sent to clients with the entire data from the chunk. + +Be aware that depending on the MC version and dimension settings biome can be set either in a 1x1x256 +column or 4x4x4 hyperblock, so for some versions Y will be ignored and for some precision of biome +setting is less than 1x1x1 block. + +Throws `unknown_biome` if the `biome_name` doesn't exist. + +### `update(pos)` + +Causes a block update at position. + +### `block_tick(pos)` + +Causes a block to tick at position. + +### `random_tick(pos)` + +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 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. + +Without item context it returns `false` if failed to destroy the block and `true` if block breaking was successful. +In item context, `true` means that breaking item has no nbt to use, `null` indicating that the tool should be +considered broken in process, and `nbt` type value, for a resulting NBT tag on a hypothetical tool. Its up to the +programmer to use that nbt to apply it where it belong + +Throws `unknown_item` if `tool` doesn't exist. + +Here is a sample code that can be used to mine blocks using items in player inventory, without using player context +for mining. Obviously, in this case the use of `harvest` would be much more applicable: + +
+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'
+  [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?');
+)
+
+ +### `harvest(player, pos)` + +Causes a block to be harvested by a specified player entity. Honors player item enchantments, as well as damages the +tool if applicable. If the entity is not a valid player, no block gets destroyed. If a player is not allowed to break +that block, a block doesn't get destroyed either. + +### `create_explosion(pos, power?, mode?, fire?, source?, attacker?)` + +Creates an explosion at a given position. Parameters work as follows: + - `'power'` - how strong the blast is, negative values count as 0 (default: `4` (TNT power)) + - `'mode'` - how to deal with broken blocks: `keep` keeps them, `destroy` destroys them and drops items, and `destroy_with_decay` destroys them, but doesn't always drop the items (default: `destroy_with_decay`) + - `fire` - whether extra fire blocks should be created (default: `false`) + - `source` - entity that is exploding. Note that it will not take explosion damage from this explosion (default: `null`) + - `attacker` - entity responsible for triggering, this will be displayed in death messages, and count towards kill counts, and can be damaged by the explosion (default: `null`) +Explosions created with this endpoint cannot be captured with `__on_explosion` event, however they will be captured +by `__on_explosion_outcome`. + +### `weather()`,`weather(type)`,`weather(type, ticks)` + +If called with no args, returns `'clear'`, `'rain` or `'thunder'` based on the current weather. If thundering, will +always return `'thunder'`, if not will return `'rain'` or `'clear'` based on the current weather. + +With one arg, (either `'clear'`, `'rain` or `'thunder'`), returns the number of remaining ticks for that weather type. +NB: It can thunder without there being a thunderstorm; there has to be both rain and thunder to form a storm. So if +running `weather()` returns `'thunder'`, you can use `weather('rain')>0` to see if there's a storm going on. + +With two args, sets the weather to the given `type` for `ticks` ticks. + +## Block and World querying + +### `pos(block), pos(entity)` + +Returns a triple of coordinates of a specified block or entity. Technically entities are queried with `query` function +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)) => [0,5,0]
+pos(player()) => [12.3, 45.6, 32.05]
+pos(block('stone')) => Error: Cannot fetch position of an unrealized block
+
+ +### `pos_offset(pos, direction, amount?)` + +Returns a coords triple that is offset in a specified `direction` by `amount` of blocks. The default offset amount is +1 block. To offset into opposite facing, use negative numbers for the `amount`. + +
+pos_offset(block(0,5,0), 'up', 2)  => [0,7,0]
+pos_offset([0,5,0], 'up', -2 ) => [0,3,0]
+
+ +### `(Deprecated) block_properties(pos)` + +Deprecated by `keys(block_state(pos))`. + +### `(Deprecated) property(pos, name)` + +Deprecated by `block_state(pos, name)` + +### `block_state(block)`, `block_state(block, property)` + +If used with a `block` argument only, it returns a map of block properties and their values. If a block has no properties, returns an +empty map. + +If `property` is specified, returns a string value of that property, or `null` if property is not applicable. + +Returned values or properties are always strings. It is expected from the user to know what to expect and convert +values to numbers using `number()` function or booleans using `bool()` function. Returned string values can be directly used +back in state definition in various applications where block properties are required. + +`block_state` can also accept block names as input, returning block's default state. + +Throws `unknown_block` if the provided input is not valid. + +
+set(x,y,z,'iron_block'); block_state(x,y,z)  => {}
+set(x,y,z,'iron_trapdoor','half','top'); block_state(x,y,z)  => {waterlogged: false, half: top, open: false, ...}
+set(x,y,z,'iron_trapdoor','half','top'); block_state(x,y,z,'half')  => top
+block_state('iron_trapdoor','half')  => top
+set(x,y,z,'air'); block_state(x,y,z,'half')  => null
+block_state(block('iron_trapdoor[half=top]'),'half')  => top
+block_state(block('iron_trapdoor[half=top]'),'powered')  => false
+bool(block_state(block('iron_trapdoor[half=top]'),'powered'))  => 0
+
+ +### `block_list()`, `block_list(tag)` + +Returns list of all blocks in the game. If `tag` is provided, returns list of all blocks that belong to this block tag. +
+block_list() => [dark_oak_button, wall_torch, structure_block, polished_blackstone_brick_slab, cherry_sapling... ]
+block_list('impermeable') => [glass, white_stained_glass, orange_stained_glass, magenta_stained_glass... ] //All da glass
+block_list('rails') => [rail, powered_rail, detector_rail, activator_rail]
+block_list('not_a_valid_block_tag') => null //Not a valid block tag
+
+ + +### `block_tags()`, `block_tags(block)`, `block_tags(block, tag)` + +Without arguments, returns list of available tags, with block supplied (either by coordinates, or via block name), returns lost +of tags the block belongs to, and if a tag is specified, returns `null` if tag is invalid, `false` if this block doesn't belong +to this tag, and `true` if the block belongs to the tag. + +Throws `unknown_block` if `block` doesn't exist + +
+block_tags() => [geode_invalid_blocks, wall_post_override, ice, wooden_stairs, bamboo_blocks, stone_bricks... ]
+block_tags('iron_block') => [mineable/pickaxe, needs_stone_tool, beacon_base_blocks]
+block_tags('glass') => [impermeable]
+block_tags('glass', 'impermeable') => true
+block_tags('glass', 'beacon_base_blocks') => false
+
+ +### `block_data(pos)` + +Return NBT string associated with specific location, or null if the block does not carry block data. Can be currently +used to match specific information from it, or use it to copy to another block + +
+block_data(x,y,z) => '{TransferCooldown:0,x:450,y:68, ... }'
+
+ +### `poi(pos), poi(pos, radius?, type?, status?, column_search?)` + +Queries a POI (Point of Interest) at a given position, returning `null` if none is found, or tuple of poi type and its +occupancy load. With optional `type`, `radius` and `status`, returns a list of POIs around `pos` within a +given `radius`. If the `type` is specified, returns only poi types of that types, or everything if omitted or `'any'`. +If `status` is specified (either `'any'`, `'available'`, or `'occupied'`) returns only POIs with that status. +With `column_search` set to `true`, it will return all POIs in a cuboid with `radius` blocks away on x and z, in the entire +block column from 0 to 255. Default (`false`) returns POIs within a spherical area centered on `pos` and with `radius` +radius. + +All results of `poi` calls are returned in sorted order with respect to the euclidean distance to the requested center of `pos`. + +The return format of the results is a list of poi type, occupancy load, and extra triple of coordinates. + +Querying for POIs using the radius is the intended use of POI mechanics, and the ability of accessing individual POIs +via `poi(pos)` in only provided for completeness. + +
+poi(x,y,z) => null  // nothing set at position
+poi(x,y,z) => ['meeting',3]  // its a bell-type meeting point occupied by 3 villagers
+poi(x,y,z,5) => []  // nothing around
+poi(x,y,z,5) => [['nether_portal',0,[7,8,9]],['nether_portal',0,[7,9,9]]] // two portal blocks in the range
+
+ +### `biome()` `biome(name)` `biome(block)` `biome(block/name, feature)`, `biome(noise_map)` + +Without arguments, returns the list of biomes in the world. + +With block, or name, returns the name of the biome in that position, or throws `'unknown_biome'` if provided biome or block are not valid. + +(1.18+) if passed a map of `continentalness`, `depth`, `erosion`, `humidity`, `temperature`, `weirdness`, returns the biome that exists at those noise values. +Note: Have to pass all 6 of the mentioned noise types and only these noise types for it to evaluate a biome. + +With an optional feature, it returns value for the specified attribute for that biome. Available and queryable features include: +* `'top_material'`: unlocalized block representing the top surface material (1.17.1 and below only) +* `'under_material'`: unlocalized block representing what sits below topsoil (1.17.1 and below only) +* `'category'`: the parent biome this biome is derived from. Possible values include (1.18.2 and below only): +`'none'`, `'taiga'`, `'extreme_hills'`, `'jungle'`, `'mesa'`, `'plains'`, `'savanna'`, +`'icy'`, `'the_end'`, `'beach'`, `'forest'`, `'ocean'`, `'desert'`, `'river'`, +`'swamp'`, `'mushroom'` , `'nether'`, `'underground'` (1.18+) and `'mountain'` (1.18+). +* `'tags'`: list of biome tags associated with this biome +* `'temperature'`: temperature from 0 to 1 +* `'fog_color'`: RGBA color value of fog +* `'foliage_color'`: RGBA color value of foliage +* `'sky_color'`: RGBA color value of sky +* `'water_color'`: RGBA color value of water +* `'water_fog_color'`: RGBA color value of water fog +* `'humidity'`: value from 0 to 1 indicating how wet is the biome +* `'precipitation'`: `'rain'` `'snot'`, or `'none'`... ok, maybe `'snow'`, but that means snots for sure as well. +* `'depth'`: (1.17.1 and below only) float value indicating how high or low the terrain should generate. Values > 0 indicate generation above sea level +and values < 0, below sea level. +* `'scale'`: (1.17.1 and below only) float value indicating how flat is the terrain. +* `'features'`: list of features that generate in the biome, grouped by generation steps +* `'structures'`: (1.17.1 and below only) list of structures that generate in the biome. + +### `solid(pos)` + +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. + +### `liquid(pos)` + +Boolean function, true if the block is liquid, or waterlogged (with any liquid). + +### `flammable(pos)` + +Boolean function, true if the block is flammable. + +### `transparent(pos)` + +Boolean function, true if the block is transparent. + +### `opacity(pos)` + +Numeric function, returning the opacity level of a block. + +### `blocks_daylight(pos)` + +Boolean function, true if the block blocks daylight. + +### `emitted_light(pos)` + +Numeric function, returning the light level emitted from the block. + +### `light(pos)` + +Numeric function, returning the total light level at position. + +### `block_light(pos)` + +Numeric function, returning the block light at position (from torches and other light sources). + +### `sky_light(pos)` + +Numeric function, returning the sky light at position (from sky access). + +### `effective_light(pos)` + +Numeric function, returning the "real" light at position, which is affected by time and weather. which also affects mobs spawning, frosted ice blocks melting. + +### `see_sky(pos)` + +Boolean function, returning true if the block can see sky. + +### `hardness(pos)` + +Numeric function, indicating hardness of a block. + +### `blast_resistance(pos)` + +Numeric function, indicating blast_resistance of a block. + +### `in_slime_chunk(pos)` + +Boolean indicating if the given block position is in a slime chunk. + +### `top(type, pos)` + +Returns the Y value of the topmost block at given x, z coords (y value of a block is not important), according to the +heightmap specified by `type`. Valid options are: + +* `light`: topmost light blocking block (1.13 only) +* `motion`: topmost motion blocking block +* `terrain`: topmost motion blocking block except leaves +* `ocean_floor`: topmost non-water block +* `surface`: topmost surface block + +
+top('motion', x, y, z)  => 63
+top('ocean_floor', x, y, z)  => 41
+
+ +### `suffocates(pos)` + +Boolean function, true if the block causes suffocation. + +### `power(pos)` + +Numeric function, returning redstone power level at position. + +### `ticks_randomly(pos)` + +Boolean function, true if the block ticks randomly. + +### `blocks_movement(pos)` + +Boolean function, true if the block at position blocks movement. + +### `block_sound(pos)` + +Returns the name of sound type made by the block at position. One of: + +`'wood'`, `'gravel'`, `'grass'`, `'stone'`, `'metal'`, `'glass'`, `'wool'`, `'sand'`, `'snow'`, +`'ladder'`, `'anvil'`, `'slime'`, `'sea_grass'`, `'coral'`, `'bamboo'`', `'shoots'`', `'scaffolding'`', `'berry'`', `'crop'`', +`'stem'`', `'wart'`', +`'lantern'`', `'fungi_stem'`', `'nylium'`', `'fungus'`', `'roots'`', `'shroomlight'`', `'weeping_vines'`', `'soul_sand'`', + `'soul_soil'`', `'basalt'`', +`'wart'`', `'netherrack'`', `'nether_bricks'`', `'nether_sprouts'`', `'nether_ore'`', `'bone'`', `'netherite'`', `'ancient_debris'`', +`'lodestone'`', `'chain'`', `'nether_gold_ore'`', `'gilded_blackstone'`', +`'candle'`', `'amethyst'`', `'amethyst_cluster'`', `'small_amethyst_bud'`', `'large_amethyst_bud'`', `'medium_amethyst_bud'`', +`'tuff'`', `'calcite'`', `'copper'`' + +### `(Deprecated) material(pos)` + +Returns `'unknown'`. The concept of material for blocks is removed. On previous versions it returned the name of the material the block +was made of. + +### `map_colour(pos)` + +Returns the map colour of a block at position. One of: + +`'air'`, `'grass'`, `'sand'`, `'wool'`, `'tnt'`, `'ice'`, `'iron'`, `'foliage'`, `'snow'`, `'clay'`, `'dirt'`, +`'stone'`, `'water'`, `'wood'`, `'quartz'`, `'adobe'`, `'magenta'`, `'light_blue'`, `'yellow'`, `'lime'`, `'pink'`, +`'gray'`, `'light_gray'`, `'cyan'`, `'purple'`, `'blue'`, `'brown'`, `'green'`, `'red'`, `'black'`, `'gold'`, +`'diamond'`, `'lapis'`, `'emerald'`, `'obsidian'`, `'netherrack'`, `'white_terracotta'`, `'orange_terracotta'`, +`'magenta_terracotta'`, `'light_blue_terracotta'`, `'yellow_terracotta'`, `'lime_terracotta'`, `'pink_terracotta'`, +`'gray_terracotta'`, `'light_gray_terracotta'`, `'cyan_terracotta'`, `'purple_terracotta'`, `'blue_terracotta'`, +`'brown_terracotta'`, `'green_terracotta'`, `'red_terracotta'`, `'black_terracotta'`, +`'crimson_nylium'`, `'crimson_stem'`, `'crimson_hyphae'`, `'warped_nylium'`, `'warped_stem'`, `'warped_hyphae'`, `'warped_wart'` + +### `sample_noise()`, `sample_noise(pos, ... types?)` 1.18+ + +Samples the world generation noise values / data driven density function(s) at a given position. + +If no types are passed in, or no arguments are given, it returns a list of all the available registry defined density functions. + +With a single function name passed in, it returns a scalar. With multiple function names passed in, it returns a list of results. + +Function accepts any registry defined density functions, both built in, as well as namespaced defined in datapacks. +On top of that, scarpet provides the following list of noises sampled directly from the current level (and not returned with no-argument call): + + +`'barrier_noise'`, `'fluid_level_floodedness_noise'`, `'fluid_level_spread_noise'`, `'lava_noise'`, +`'temperature'`, `'vegetation'`, `'continents'`, `'erosion'`, `'depth'`, `'ridges'`, +`'initial_density_without_jaggedness'`, `'final_density'`, `'vein_toggle'`, `'vein_ridged'` and `'vein_gap'` + +
+// requesting single value
+sample_density(pos, 'continents') => 0.211626790923
+// passing type as multiple arguments
+sample_density(pos, 'continents', 'depth', 'overworld/caves/pillars', 'mydatapack:foo/my_function') => [-0.205013844481, 1.04772473438, 0.211626790923, 0.123]
+
+ +### `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 an advantage. Vanilla `fill/clone` +commands only check the specified corners for loadness. + +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(player()))  => 1
+loaded(100000,100,1000000)  => 0
+
+ +### `(Deprecated) loaded_ep(pos)` + +Boolean function, true if the block is loaded and entity processing, as per 1.13.2 + +Deprecated as of scarpet 1.6, use `loaded_status(x) > 0`, or just `loaded(x)` with the same effect + +### `loaded_status(pos)` + +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)` + +Returns `true` if the region file for the chunk exists, +`false` otherwise. If optional force is `true` it will also check if the chunk has a non-empty entry in its region file +Can be used to assess if the chunk has been touched by the game or not. + +`generation_status(pos, false)` only works on currently loaded chunks, and `generation_status(pos, true)` will create +an empty loaded chunk, even if it is not needed, so `is_chunk_generated` can be used as a efficient proxy to determine +if the chunk physically exists. + +Running `is_chunk_generated` is has no effects on the world, but since it is an external file operation, it is +considerably more expensive (unless area is loaded) than other generation and loaded checks. + +### `generation_status(pos), generation_status(pos, true)` + +Returns generation status as per the ticket system. Can return any value from several available but chunks +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) + +### `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([pos, ...])` +Removes and resets the chunk, all chunks in the specified area or all chunks in a list at once, removing all previous +blocks and entities, and replacing it with a new generation. For all currently loaded chunks, they will be brought +to their current generation status, and updated to the player. All chunks that are not in the loaded area, will only +be generated to the `'structure_starts'` status, allowing to generate them fully as players are visiting them. +Chunks in the area that has not been touched yet by the game will not be generated / regenerated. + +It returns a `map` with a report indicating how many chunks were affected, and how long each step took: + * `requested_chunks`: total number of chunks in the requested area or list + * `affected_chunks`: number of chunks that will be removed / regenerated + * `loaded_chunks`: number of currently loaded chunks in the requested area / list + * `relight_count`: number of relit chunks + * `relight_time`: time took to relit chunks + * `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. + +## Structure and World Generation Features API + +Scarpet provides convenient methods to access and modify information about structures as well as spawn in-game +structures and features. List of available options and names that you can use depends mostly if you are using scarpet +with minecraft 1.16.1 and below or 1.16.2 and above since in 1.16.2 Mojang has added JSON support for worldgen features +meaning that since 1.16.2 - they have official names that can be used by datapacks and scarpet. If you have most recent +scarpet on 1.16.4, you can use `plop()` to get all available worldgen features including custom features and structures +controlled by datapacks. It returns a map of lists in the following categories: +`'scarpet_custom'`, `'configured_features'`, `'structures'`, `'features'`, `'structure_types'` + +### Previous Structure Names, including variants (for MC1.16.1 and below only) +* `'monument'`: Ocean Monument. Generates at fixed Y coordinate, surrounds itself with water. +* `'fortress'`: Nether Fortress. Altitude varies, but its bounded by the code. +* `'mansion'`: Woodland Mansion +* `'jungle_temple'`: Jungle Temple +* `'desert_temple'`: Desert Temple. Generates at fixed Y altitude. +* `'endcity'`: End City with Shulkers (in 1.16.1- as `'end_city`) +* `'igloo'`: Igloo +* `'shipwreck'`: Shipwreck +* `'shipwreck2'`: Shipwreck, beached +* `'witch_hut'` +* `'ocean_ruin'`, `ocean_ruin_small'`, `ocean_ruin_tall'`: Stone variants of ocean ruins. +* `'ocean_ruin_warm'`, `ocean_ruin_warm_small'`, `ocean_ruin_warm_tall'`: Sandstone variants of ocean ruins. +* `'treasure'`: A treasure chest. Yes, its a whole structure. +* `'pillager_outpost'`: A pillager outpost. +* `'mineshaft'`: A mineshaft. +* `'mineshaft_mesa'`: A Mesa (Badlands) version of a mineshaft. +* `'village'`: Plains, oak village. +* `'village_desert'`: Desert, sandstone village. +* `'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 Names (1.16.1 and below) + +* `'oak'` +* `'oak_beehive'`: oak with a hive (1.15+). +* `'oak_large'`: oak with branches. +* `'oak_large_beehive'`: oak with branches and a beehive (1.15+). +* `'birch'` +* `'birch_large'`: tall variant of birch tree. +* `'shrub'`: low bushes that grow in jungles. +* `'shrub_acacia'`: low bush but configured with acacia (1.14 only) +* `'shrub_snowy'`: low bush with white blocks (1.14 only) +* `'jungle'`: a tree +* `'jungle_large'`: 2x2 jungle tree +* `'spruce'` +* `'spruce_large'`: 2x2 spruce tree +* `'pine'`: spruce with minimal leafage (1.15+) +* `'pine_large'`: 2x2 spruce with minimal leafage (1.15+) +* `'spruce_matchstick'`: see 1.15 pine (1.14 only). +* `'spruce_matchstick_large'`: see 1.15 pine_large (1.14 only). +* `'dark_oak'` +* `'acacia'` +* `'oak_swamp'`: oak with more leaves and vines. +* `'well'`: desert well +* `'grass'`: a few spots of tall grass +* `'grass_jungle'`: little bushier grass feature (1.14 only) +* `'lush_grass'`: grass with patchy ferns (1.15+) +* `'tall_grass'`: 2-high grass patch (1.15+) +* `'fern'`: a few random 2-high ferns +* `'cactus'`: random cacti +* `'dead_bush'`: a few random dead bushi +* `'fossils'`: underground fossils, placement little wonky +* `'mushroom_brown'`: large brown mushroom. +* `'mushroom_red'`: large red mushroom. +* `'ice_spike'`: ice spike. Require snow block below to place. +* `'glowstone'`: glowstone cluster. Required netherrack above it. +* `'melon'`: a patch of melons +* `'melon_pile'`: a pile of melons (1.15+) +* `'pumpkin'`: a patch of pumpkins +* `'pumpkin_pile'`: a pile of pumpkins (1.15+) +* `'sugarcane'` +* `'lilypad'` +* `'dungeon'`: Dungeon. These are hard to place, and fail often. +* `'iceberg'`: Iceberg. Generate at sea level. +* `'iceberg_blue'`: Blue ice iceberg. +* `'lake'` +* `'lava_lake'` +* `'end_island'` +* `'chorus'`: Chorus plant. Require endstone to place. +* `'sea_grass'`: a patch of sea grass. Require water. +* `'sea_grass_river'`: a variant. +* `'kelp'` +* `'coral_tree'`, `'coral_mushroom'`, `'coral_claw'`: various coral types, random color. +* `'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) + + +### World Generation Features and Structures (as of MC1.16.2+) + +Use `plop():'structure_types'`, `plop():'structures'`, `plop():'features'`, and `plop():'configured_features'` for a list of available options. Your output may vary based on +datapacks installed in your world. + +### Custom Scarpet Features + +Use `plop():'scarpet_custom'` for a full list. + +These contain some popular features and structures that are impossible or difficult to obtain with vanilla structures/features. + +* `'bastion_remnant_bridge'` - Bridge version of a bastion remnant +* `'bastion_remnant_hoglin_stable'` - Hoglin stables version of a bastion remnant +* `'bastion_remnant_treasure'` - Treasure version of a bastion remnant +* `'bastion_remnant_units'` - Housing units version of a bastion remnant +* `'birch_bees'` - birch tree that always generates with a beehive unlike standard that generate with probability +* `'coral'` - random standalone coral feature, typically part of `'warm_ocean_vegetation'` +* `'coral_claw'` - claw coral feature +* `'coral_mushroom'` - mushroom coral feature +* `'coral_tree'` - tree coral feature +* `'fancy_oak_bees'` - large oak tree variant with a mandatory beehive unlike standard that generate with probability +* `'oak_bees'` - normal oak tree with a mandatory beehive unlike standard that generate with probability + + +### `structure_eligibility(pos, ?structure, ?size_needed)` + +Checks worldgen eligibility for a structure in a given chunk. Requires a `Structure Variant` name (see above), +or `Standard Structure` to check structures of this type. +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 or invalid, `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, or a `Standard Structure` was specified, like `'village'`,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. + +Throws `unknown_structure` if structure doesn't exist. + +### `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 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. If structure is invalid, its data will be `null`. + +Requires a `Standard Structure` name (see above). + +### `structure_references(pos), structure_references(pos, structure_name)` + +Returns structure information that a chunk with a given block position is part of. `structure_references` function +can be called with a block, or a block and a structure name. In the first case it returns a list of structure names +that give chunk belongs to. When called with an extra structure name, returns list of positions pointing to the +lowest block position in chunks that hold structure starts for these structures. You can query that chunk structures +then to get its bounding boxes. + +Requires a `Standard Structure` name (see above). + +### `set_structure(pos, structure_name), set_structure(pos, structure_name, null)` + +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 the structure a given point is in, use `structure_references` to find where current +structure starts. + +Requires a `Structure Variant` or `Standard Structure` name (see above). If standard name is used, the variant of the +structure may depend on the biome, otherwise the default structure for this type will be generated. + +Throws `unknown_structure` if structure doesn't exist. + +### `plop(pos, what)` + +Plops a structure or a feature at a given `pos`, so block, triple position coordinates or a list of coordinates. +To `what` gets plopped and exactly where it often depends on the feature or structure itself. + +Requires a `Structure Type`, `Structure`, `World Generation Feature` or `Custom Scarpet Feature` name (see +above). If standard name is used, the variant of the structure may depend on the biome, otherwise the default +structure for this type will be generated. + +All structures are chunk aligned, and often span multiple chunks. Repeated calls to plop a structure in the same chunk +would result either in the same structure generated on top of each other, or with different state, but same position. +Most structures generate at specific altitudes, which are hardcoded, or with certain blocks around them. API will +cancel all extra position / biome / random requirements for structure / feature placement, but some hardcoded +limitations may still cause some of structures/features not to place. Some features require special blocks to be +present, like coral -> water or ice spikes -> snow block, and for some features, like fossils, placement is all sorts +of messed up. This can be partially avoided for structures by setting their structure information via `set_structure`, +which sets it without looking into world blocks, and then use `plop` to fill it with blocks. This may, or may not work. + +All generated structures will retain their properties, like mob spawning, however in many cases the world / dimension +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. +# Iterating over larger areas of blocks + +These functions help scan larger areas of blocks without using generic loop functions, like nested `loop`. + +### `scan(center, range, upper_range?, expr)` + +Evaluates expression over area of blocks defined by its center `center = (cx, cy, cz)`, expanded in all directions +by `range = (dx, dy, dz)` blocks, or optionally in negative with `range` coords, and `upper_range` coords in +positive values. That means that if you want a box starting at the northwest coord with given base, width and height +dimensions, you can do `'scan(center, 0, 0, 0, w, h, d, ...)`. + +`center` can be defined either as three coordinates, a single tuple of three coords, or a block value. +`range` and `upper_range` can have the same representations, just if they are block values, it computes the distance to +the center as range instead of taking the values as is. That way you can iterate from the center to a box whose surface +area constains the `range` and/or `upper_range` blocks. + +`expr` receives `_x, _y, _z` variables 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. + +`scan` also handles `continue` and `break` statements, using `continue`'s return value to use in place of expression +return value. `break` return value has no effect. + +### `volume(from_pos, to_pos, expr)` + +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. +You can use a position or three coordinates to specify, it doesn't matter. + +For return value and handling `break` and `continue` statements, see `scan` function above. + +### `neighbours(pos)` + +Returns the list of 6 neighbouring blocks to the argument. Commonly used with other loop functions like `for`. + +
+for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block
+
+ +### `rect(center, range?, upper_range?)` + +Returns an iterator, just like `range` function that iterates over a rectangular area of blocks. If only center +point is specified, it iterates over 27 blocks (range of 1). If `range` arguments are specified, expands selection by +the respective number of blocks in each direction. If `upper_range` arguments are specified, it uses `range` for +negative offset, and `upper_range` for positive, similar to `scan`. + +Basically the arguments are the same as the first three arguments of `scan`, except this function returns the list of +blocks that `scan` would evaluate over. If you are going to iterate over these blocks, like `for(rect(args), do_something())`, +then `scan(args, do_something())` is an equivalent, yet more compute-friendly alternative, especially for very large areas. + +`center` can be defined either as three coordinates, a list of three coords, or a block value. +`range` and `upper_range` can have the same representations, just if they are block values, it computes the distance to the center +as range instead of taking the values as is.` + +### `diamond(center_pos, radius?, height?)` + +Iterates over a diamond like area of blocks. With no radius and height, its 7 blocks centered around the middle +(block + neighbours). With a radius specified, it expands shape on x and z coords, and with a custom height, on y. +Any of these can be zero as well. radius of 0 makes a stick, height of 0 makes a diamond shape pad. + +If radius and height are the same, creats a 3D diamond, of all the blocks which are a manhattan distance of `radius` away +from the center. +# Entity API + +## Entity Selection + +Entities have to be fetched before using them. Entities can also change their state between calls to the script if +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. +For player-scoped apps (which is a default) its always the owning player or `null` if it not present even if some code +still runs in their name. +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, +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 he/she is logged in. + +### `entity_id(uuid), entity_id(id)` + +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. + +### `entity_list(descriptor)` + +Returns global lists of entities in the current dimension matching specified descriptor. +Calls to `entity_list` always fetch entities from the current world that the script executes. + +### `entity_types(descriptor)` + +Resolves a given descriptor returning list of entity types that match it. The returned list of types is also a valid list +of descriptors that can be use elsewhere where entity types are required. + +Currently, the following descriptors are available: + +* `*`: all entities, even `!valid`, matches all entity types. +* `valid` - all entities that are not dead (health > 0). All main categories below also return only +entities in the `valid` category. matches all entity types. `!valid` matches all entites that are already dead of all types. +* `living` - all entities that resemble a creature of some sort +* `projectile` - all entities or types that are not living that can be throw or projected, `!projectile` matches all types that + are not living, but cannot the thrown or projected. +* `minecarts` matches all minecart types. `!minecarts` matches all types that are not live, but also not minecarts. Using plural +since `minecart` is a proper entity type on its own. +* `undead`, `arthropod`, `aquatic`, `regular`, `illager` - all entities / types that belong to any of these groups. All +living entities belong to one and only one of these. Corresponding negative (e.g. `!undead`) corresponds to all mobs that are +living but don't belong to that group. Entity groups are used in interaction / battle mechanics like smite for undead, or impaling +for aquatic. Also certain mechanics interact with groups, like ringing a bell with illagers. All other mobs that don't have any of these traits belong +to the `regular` group. +* `monster`, `creature`, `ambient`, `water_creature`, `water_ambient`, `misc` - another categorization of +living entities based on their spawn group. Negative descriptor resolves to all living types that don't belong to that +category. +* All entity tags including those provided with datapacks. Built-in entity tags include: `skeletons`, `raiders`, +`beehive_inhabitors` (bee, duh), `arrows` and `impact_projectiles`. +* Any of the standard entity types, equivalent to selection from `/summon` vanilla command, which is one of the options returned +by `entity_types()`, except for `'fishing_bobber'` and `'player'`. + +All categories can be preceded with `'!'` which will fetch all entities (unless otherwise noted) that are valid (health > 0) but not +belonging to that group. + +### `entity_area(type, center, distance)` + + +Returns entities of a specified type in an area centered on `center` and at most `distance` blocks away from +the center point/area. Uses the same `type` selectors as `entities_list`. + +`center` and `distance` can either be a triple of coordinates or three consecutive arguments for `entity_area`. `center` can +also be represented as a block, in this case the search box will be centered on the middle of the block, or an entity - in this case +entire bounding box of the entity serves as a 'center' of search which is then expanded in all directions with the `'distance'` vector. + +In any case - returns all entities which bounding box collides with the bounding box defined by `'center'` and `'distance'`. + +entity_area is simpler than `entity_selector` and runs about 20% faster, but is limited to predefined selectors and +cuboid search area. + +### `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 it should be as fast as other methods of selecting entities. Unlike other +entities fetching / filtering method, this one doesn't guarantee to return entities from current dimension, since +selectors can return any loaded entity in the world. + +### `spawn(name, pos, nbt?)` + +Spawns and places an entity in world, like `/summon` vanilla command. Requires a position to spawn, and optional +extra nbt data to merge with the entity. What makes it different from calling `run('summon ...')`, is the fact that +you get the entity back as a return value, which is swell. + +## Entity Manipulation + +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: + +
+query(p, 'name') <=> p ~ 'name'     // much shorter and cleaner
+query(p, 'holds', 'offhand') <=> p ~ ['holds', 'offhand']    // not really but can be done
+
+ +### `query(e, 'removed')` + +Boolean. True if the entity is removed. + +### `query(e, 'id')` + +Returns numerical id of the entity. Most efficient way to keep track of entities 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). + +### `query(e, 'uuid')` + +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 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')` + +Triple of the entity's position + +### `query(e, 'location')` + +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')` + +Respective component of entity's coordinates + +### `query(e, 'pitch')`, `query(e, 'yaw')` + +Pitch and Yaw or where entity is looking. + +### `query(e, 'head_yaw')`, `query(e, 'body_yaw')` + +Applies to living entites. Sets their individual head and body facing angle. + +### `query(e, 'look')` + +Returns a 3d vector where the entity is looking. + +### `query(e, 'motion')` + +Triple of entity's motion vector, `[motion_x, motion_y, motion_z]`. Motion represents the velocity from all the forces +that exert on the given entity. Things that are not 'forces' like voluntary movement, or reaction from the ground are +not part of said forces. + +### `query(e, 'motion_x'), query(e, 'motion_y'), query(e, 'motion_z')` + +Respective component of the entity's motion vector + +### `query(e, 'on_ground')` + +Returns `true` if an entity is standing on firm ground and falling down due to that. + +### `query(e, 'name'), query(e, 'display_name'), query(e, 'custom_name'), query(e, 'type')` + +String of entity name or formatted text in the case of `display_name` + +
+query(e,'name')  => Leatherworker
+query(e,'custom_name')  => null
+query(e,'type')  => villager
+
+ +### `query(e, 'command_name')` + +Returns a valid string to be used in commands to address an entity. Its UUID for all entities except +player, where its their name. + +
+run('/kill ' + e~'command_name');
+
+ +### `query(e, 'persistence')` + +Returns if a mob has a persistence tag or not. Returns `null` for non-mob entities. + +### `query(e, 'is_riding')` + +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')` + +Entity that `e` rides. + +### `query(e, 'unmountable')` + +Boolean, true if the entity cannot be mounted. + +### `(deprecated) query(e, 'tags')` + +Deprecated by `query(e, 'scoreboard_tags')` + +### `query(e, 'scoreboard_tags')` + +List of entity's scoreboard tags. + +### `(deprecated) query(e, 'has_tag',tag)` + +Deprecated by `query(e, 'has_scoreboard_tag',tag)` + +### `query(e, 'has_scoreboard_tag',tag)` + +Boolean, true if the entity is marked with a `tag` scoreboad tag. + +### `query(e, 'entity_tags')` + +List of entity tags assigned to the type this entity represents. + +### `query(e, 'has_entity_tag', tag)` + +Returns `true` if the entity matches that entity tag, `false` if it doesn't, and `null` if the tag is not valid. + +### `query(e, 'is_burning')` + +Boolean, true if the entity is burning. + +### `query(e, 'fire')` + +Number of remaining ticks of being on fire. + +### `query(e, 'is_freezing')` + +Boolean, true if the entity is freezing. + +### `query(e, 'frost')` + +Number of remaining ticks of being frozen. + +### `query(e, 'silent')` + +Boolean, true if the entity is silent. + +### `query(e, 'gravity')` + +Boolean, true if the entity is affected by gravity, like most entities are. + +### `query(e, 'invulnerable')` + +Boolean, true if the entity is invulnerable. + +### `query(e, 'immune_to_fire')` + +Boolean, true if the entity is immune to fire. + +### `query(e, 'immune_to_frost')` + +Boolean, true if the entity is immune to frost. + +### `query(e, 'dimension')` + +Name of the dimension the entity is in. + +### `query(e, 'height')` + +Height of the entity in blocks. + +### `query(e, 'width')` + +Width of the entity in blocks. + +### `query(e, 'eye_height')` + +Eye height of the entity in blocks. + +### `query(e, 'age')` + +Age of the entity in ticks, i.e. how long it existed. + +### `query(e, 'breeding_age')` + +Breeding age of passive entity, in ticks. If negative, time to adulthood, if positive, breeding cooldown. + +### `query(e, 'despawn_timer')` + +For living entities, the number of ticks they fall outside of immediate player presence. + +### `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 or item frame entity, `null` otherwise. + +### `query(e, 'offering_flower')` + +Whether the given iron golem has a red flower in their hand. returns null for all other entities + + +### `query(e, 'blue_skull')` + +Whether the given wither skull entity is blue. returns null for all other entities + + +### `query(e, 'count')` + +Number of items in a stack from item entity, `null` otherwise. + +### `query(e, 'pickup_delay')` + +Retrieves pickup delay timeout for an item entity, `null` otherwise. + +### `query(e, 'is_baby')` + +Boolean, true if its a baby. + +### `query(e, 'target')` + +Returns mob's attack target or null if none or not applicable. + +### `query(e, 'home')` + +Returns creature's home position (as per mob's AI, leash etc) or null if none or not applicable. + +### `query(e, 'spawn_point')` + +Returns position tuple, dimension, spawn angle, and whether spawn is forced, assuming the player has a spawn position. +Returns `false` if spawn position is not set, and `null` if `e` is not a player. + +### `query(e, 'path')` + +Returns path of the entity if present, `null` otherwise. The path comprises of list of nodes, each is a list +of block value, node type, penalty, and a boolean indicated if the node has been visited. + +### `query(e, 'pose')` + +Returns a pose of an entity, one of the following options: + * `'standing'` + * `'fall_flying'` + * `'sleeping'` + * `'swimming'` + * `'spin_attack'` + * `'crouching'` + * `'dying'` + +### `query(e, 'sneaking')` + +Boolean, true if the entity is sneaking. + +### `query(e, 'sprinting')` + +Boolean, true if the entity is sprinting. + +### `query(e, 'swimming')` + +Boolean, true if the entity is swimming. + +### `query(e, 'jumping')` + +Boolean, true if the entity is jumping. + +### `query(e, 'swinging')` + +Returns `true` if the entity is actively swinging their hand, `false` if not and `null` if swinging is not applicable to +that entity. + +### `query(e, 'gamemode')` + +String with gamemode, or `null` if not a player. + +### `query(e, 'gamemode_id')` + +Good'ol gamemode id, or null if not a player. + +### `query(e, 'player_type')` + +Returns `null` if the argument is not a player, otherwise: + +* `singleplayer`: for singleplayer game +* `multiplayer`: for players on a dedicated server +* `lan_host`: for singleplayer owner that opened the game to LAN +* `lan_player`: for all other players that connected to a LAN host +* `fake`: any carpet-spawned fake player +* `shadow`: any carpet-shadowed real player +* `realms`: ? + +### `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')` + +Player's permission level, or `null` if not applicable for this entity. + +### `query(e, 'client_brand')` + +Returns recognized type of client of the client connected. Possible results include `'vanilla'`, or `'carpet '` where +version indicates the version of the connected carpet client. + +### `query(e, 'effect', name?)` + +Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short +effect name, amplifier, and remaining duration in ticks (-1 if it has infinity duration). With an argument, if the living entity has not that potion active, +returns `null`, otherwise return a tuple of amplifier and remaining duration. + +
+query(p,'effect')  => [[haste, 0, 177], [speed, 0, 177]]
+query(p,'effect','haste')  => [0, 177]
+query(p,'effect','resistance')  => null
+
+ +### `query(e, 'health')` + +Number indicating remaining entity health, or `null` if not applicable. + +### `query(e, 'may_fly')` + +Returns a boolean indicating if the player can fly. + +### `query(e, 'flying')` + +Returns a boolean indicating if the player is flying. + +### `query(e, 'may_build')` + +Returns a boolean indicating if the player is allowed to place blocks. + +### `query(e, 'insta_build')` + +Returns a boolean indicating if the player can place blocks without consuming the item and if the player can shoot arrows without having them in the inventory. + +### `query(e, 'fly_speed')` + +Returns a number indicating the speed at which the player moves while flying. + +### `query(e, 'walk_speed')` + +Returns a number indicating the speed at which the player moves while walking. + +### `query(e, 'hunger')` +### `query(e, 'saturation')` +### `query(e, 'exhaustion')` + +Retrieves player hunger related information. For non-players, returns `null`. + +### `query(e, 'absorption')` + +Gets the absorption of the player (yellow hearts, e.g. when having a golden apple.) + +### `query(e, 'xp')` +### `query(e, 'xp_level')` +### `query(e, 'xp_progress')` +### `query(e, 'score')` + +Numbers related to player's xp. `xp` is the overall xp player has, `xp_level` is the levels seen in the hotbar, +`xp_progress` is a float between 0 and 1 indicating the percentage of the xp bar filled, and `score` is the number displayed upon death + +### `query(e, 'air')` + +Number indicating remaining entity air, or `null` if not applicable. + +### `query(e, 'language')` + +Returns `null` for any non-player entity, if not returns the player's language as a string. + +### `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` +* `head` +* `chest` +* `legs` +* `feet` + +If `slot` is not specified, it defaults to the main hand. + +### `query(e, 'selected_slot')` + +Number indicating the selected slot of entity's inventory. Currently only applicable to players. + +### `query(e, 'active_block')` + +Returns currently mined block by the player, as registered by the game server. + +### `query(e, 'breaking_progress')` + +Returns current breaking progress of a current player mining block, or `null`, if no block is mined. +Breaking progress, if not null, is any number 0 or above, while 10 means that the block should already be +broken by the client. This value may tick above 10, if the client / connection is lagging. + +Example: + +The following program provides custom breaking times, including nice block breaking animations, including instamine, for +blocks that otherwise would take longer to mine. + +[Video demo](https://youtu.be/zvEEuGxgCio) +```py +global_blocks = { + 'oak_planks' -> 0, + 'obsidian' -> 1, + 'end_portal_frame' -> 5, + 'bedrock' -> 10 +}; + +__on_player_clicks_block(player, block, face) -> +( + step = global_blocks:str(block); + if (step == 0, + destroy(block, -1); // instamine + , step != null, + schedule(0, '_break', player, pos(block), str(block), step, 0); + ) +); + +_break(player, pos, name, step, lvl) -> +( + current = player~'active_block'; + if (current != name || pos(current) != pos, + modify(player, 'breaking_progress', null); + , + modify(player, 'breaking_progress', lvl); + if (lvl >= 10, destroy(pos, -1)); + schedule(step, '_break', player, pos, name, step, lvl+1) + ); +) +``` + +### `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?...)` + +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 +action. This can be customized with `options`, use `'blocks'` to trace for blocks, `'liquids'` to include liquid blocks +as possible results, and `'entities'` to trace entities. You can also specify `'exact'` which returns the actual hit + coordinate as a triple, instead of a block or entity value. Any combination of the above is possible. When tracing +entities and blocks, blocks will take over the priority even if transparent or non-colliding +(aka fighting chickens in tall grass). + +Regardless of the options selected, the result could be: + - `null` if nothing is in reach + - an entity if look targets an entity + - block value if block is in reach, or + - a coordinate triple if `'exact'` option was used and hit was successful. + +### `query(e, 'attribute')` `query(e, 'attribute', name)` + +returns the value of an attribute of the living entity. If the name is not provided, +returns a map of all attributes and values of this entity. If an attribute doesn't apply to the entity, +or the entity is not a living entity, `null` is returned. + +### `query(e, 'brain', memory)` + +Retrieves brain memory for entity. Possible memory units highly depend on the game version. Brain is availalble +for villagers (1.15+) and Piglins, Hoglins, Zoglins and Piglin Brutes (1.16+). If memory is not present or +not available for the entity, `null` is returned. + +Type of the returned value (entity, position, number, list of things, etc) depends on the type of the requested +memory. On top of that, since 1.16, memories can have expiry - in this case the value is returned as a list of whatever +was there, and the current ttl in ticks. + +Available retrievable memories for 1.15.2: +* `home`, `job_site`, `meeting_point`, `secondary_job_site`, `mobs`, `visible_mobs`, `visible_villager_babies`, +`nearest_players`, `nearest_visible_player`, `walk_target`, `look_target`, `interaction_target`, +`breed_target`, `path`, `interactable_doors`, `opened_doors`, `nearest_bed`, `hurt_by`, `hurt_by_entity`, +`nearest_hostile`, `hiding_place`, `heard_bell_time`, `cant_reach_walk_target_since`, +`golem_last_seen_time`, `last_slept`, `last_woken`, `last_worked_at_poi` + +Available retrievable memories as of 1.16.2: +* `home`, `job_site`, `potential_job_site`, `meeting_point`, `secondary_job_site`, `mobs`, `visible_mobs`, +`visible_villager_babies`, `nearest_players`, `nearest_visible_players`, `nearest_visible_targetable_player`, +`walk_target`, `look_target`, `attack_target`, `attack_cooling_down`, `interaction_target`, `breed_target`, +`ride_target`, `path`, `interactable_doors`, `opened_doors`, `nearest_bed`, `hurt_by`, `hurt_by_entity`, `avoid_target`, +`nearest_hostile`, `hiding_place`, `heard_bell_time`, `cant_reach_walk_target_since`, `golem_detected_recently`, +`last_slept`, `last_woken`, `last_worked_at_poi`, `nearest_visible_adult`, `nearest_visible_wanted_item`, +`nearest_visible_nemesis`, `angry_at`, `universal_anger`, `admiring_item`, `time_trying_to_reach_admire_item`, +`disable_walk_to_admire_item`, `admiring_disabled`, `hunted_recently`, `celebrate_location`, `dancing`, +`nearest_visible_huntable_hoglin`, `nearest_visible_baby_hoglin`, `nearest_targetable_player_not_wearing_gold`, +`nearby_adult_piglins`, `nearest_visible_adult_piglins`, `nearest_visible_adult_hoglins`, +`nearest_visible_adult_piglin`, `nearest_visible_zombiefied`, `visible_adult_piglin_count`, +`visible_adult_hoglin_count`, `nearest_player_holding_wanted_item`, `ate_recently`, `nearest_repellent`, `pacified` + + +### `query(e, 'nbt', path?)` + +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 +be used when there is no other option. Returned value is of type `nbt`, which can be further manipulated with nbt +type objects via `get, put, has, delete`, so try to use API calls first for that. + +## Entity Modification + +Like with entity querying, entity modifications happen through one function. + +### `modify(e, 'remove')` + +Removes (not kills) entity from the game. + +### `modify(e, 'kill')` + +Kills the entity. + +### `modify(e, 'pos', x, y, z), modify(e, 'pos', [x,y,z] )` + +Moves the entity to a specified coords. + +### `modify(e, 'location', x, y, z, yaw, pitch), modify(e, 'location', [x, y, z, yaw, pitch] )` + +Changes full location vector all at once. + +### `modify(e, 'x', x), modify(e, 'y', y), modify(e, 'z', z)` + +Changes entity's location in the specified direction. + +### `modify(e, 'pitch', angle), modify(e, 'yaw', angle)` + +Changes entity's pitch or yaw angle. + +### `modify(e, 'look', x, y, z), modify(e, 'look', [x,y,z] )` + +Sets entity's 3d vector where the entity is looking. +For cases where the vector has a length of 0, yaw and pitch won't get changed. +When pointing straight up or down, yaw will stay the same. + +### `modify(e, 'head_yaw', angle)`, `modify(e, 'body_yaw', angle)` + +For living entities, controls their head and body yaw angle. + +### `modify(e, 'move', x, y, z), modify(e, 'move', [x,y,z] )` + +Moves the entity by a vector from its current location. + +### `modify(e, 'motion', x, y, z), modify(e, 'motion', [x,y,z] )` + +Sets the motion vector (where and how much entity is moving). + +### `modify(e, 'motion_x', x), modify(e, 'motion_y', y), modify(e, 'motion_z', z)` + +Sets the corresponding component of the motion vector. + +### `modify(e, 'accelerate', x, y, z), modify(e, 'accelerate', [x, y, z] )` + +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', name, visible)` + +Sets the custom name of the entity. Without arguments - clears current custom name. Optional visible affects +if the custom name is always visible, even through blocks. + +### `modify(e, 'persistence', bool?)` + +Sets the entity persistence tag to `true` (default) or `false`. Only affects mobs. Persistent mobs +don't despawn and don't count towards the mobcap. + +### `modify(e, 'item', item_triple)` + +Sets the item for the item or item frame entity. (The item triple is a list of `[item_name, count, nbt]`, or just an item name.) + +### `modify(e, 'offering_flower', bool)` + +Sets if the iron golem has a red flower in hand. + +### `modify(e, 'blue_skull', bool)` + +Sets whether the wither skull entity is blue. + +### `modify(e, 'age', number)` + +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. + +### `modify(e, 'mount', other)` + +Mounts the entity to the `other`. + +### `modify(e, 'unmountable', boolean)` + +Denies or allows an entity to be mounted. + +### `modify(e, 'drop_passengers')` + +Shakes off all passengers. + +### `modify(e, 'mount_passengers', passenger, ? ...), modify(e, 'mount_passengers', [passengers] )` + +Mounts on all listed entities on `e`. + +### `modify(e, 'tag', tag, ? ...), modify(e, 'tag', [tags] )` + +Adds tag(s) to the entity. + +### `modify(e, 'clear_tag', tag, ? ...), modify(e, 'clear_tag', [tags] )` + +Removes tag(s) from the entity. + +### `modify(e, 'talk')` + +Make noises. + +### `modify(e, 'ai', boolean)` + +If called with `false` value, it will disable AI in the mob. `true` will enable it again. + +### `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. + +### `modify(e, 'effect', name?, duration?, amplifier?, show_particles?, show_icon?, ambient?)` + +Applies status effect to the living entity. Takes several optional parameters, which default to `0`, `true`, +`true` and `false`. If no duration is specified, or if it's null or 0, the effect is removed. If duration is less than 0, it will represent infinity. If name is not specified, +it clears all effects. + +### `modify(e, 'health', float)` + +Modifies the health of an entity. + +### `modify(e, 'may_fly', boolean)` + +Allows or denies the player the ability to fly. If the player is flying and the ability is removed, the player will stop flying. + +### `modify(e, 'flying', boolean)` + +Changes the flight status of the player (if it is flying or not). + +### `modify(e, 'may_build', boolean)` + +Allows or denies the player the ability to place blocks. + +### `modify(e, 'insta_build', boolean)` + +Allows or denies the player to place blocks without reducing the item count of the used stack and to shoot arrows without having them in the inventory. + +### `modify(e, 'fly_speed', float)` + +Modifies the value of the speed at which the player moves while flying. + +### `modify(e, 'walk_speed', float)` + +Modifies the value of the speed at which the player moves while walking. + +### `modify(e, 'selected_slot', int)` + +Changes player's selected slot. + +### `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, 'spawn_point')`, `modify(e, 'spawn_point', null)`, `modify(e, 'spawn_point', pos, dimension?, angle?, forced?)` + +Changes player respawn position to given position, optional dimension (defaults to current player dimension), angle (defaults to +current player facing) and spawn forced/fixed (defaults to `false`). If `none` or nothing is passed, the respawn point +will be reset (as removed) instead. + +### `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, 'swing')` `modify(e, 'swing', 'offhand')` + +Makes the living entity swing their required limb. + +### `modify(e, 'silent', boolean)` + +Silences or unsilences the entity. + +### `modify(e, 'gravity', boolean)` + +Toggles gravity for the entity. + +### `modify(e, 'invulnerable', boolean)` + +Toggles invulnerability for the entity. + +### `modify(e, 'fire', ticks)` + +Will set entity on fire for `ticks` ticks. Set to 0 to extinguish. + +### `modify(e, 'frost', ticks)` + +Will give entity frost for `ticks` ticks. Set to 0 to unfreeze. + +### `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, 'absorption', value)` + +Sets the absorption value for the player. Each point is half a yellow heart. + +### `modify(e, 'add_xp', value)` +### `modify(e, 'xp_level', value)` +### `modify(e, 'xp_progress', value)` +### `modify(e, 'xp_score', value)` + +Manipulates player xp values - `'add_xp'` the method you probably want to use +to manipulate how much 'xp' an action should give. `'xp_score'` only affects the number you see when you die, and +`'xp_progress'` controls the xp progressbar above the hotbar, should take values from 0 to 1, but you can set it to any value, +maybe you will get a double, who knows. + +### `modify(e, 'air', ticks)` + +Modifies entity air. + +### `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' an action costs. + +### `modify(e, 'breaking_progress', value)` + +Modifies the breaking progress of a player currently mined block. Value of `null`, `-1` makes it reset. +Values `0` to `10` will show respective animation of a breaking block. Check `query(e, 'breaking_progress')` for +examples. + +### `modify(e, 'nbt_merge', partial_tag)` + +Merges a partial tag into the entity data and reloads the entity from its updated tag. Cannot be applied to players. + +### `modify(e, 'nbt', tag)` + +Reloads the entity from a supplied tag. Better use a valid entity tag, what can go wrong? Wonder what would happen if you +transplant rabbit's brain into a villager? Cannot be applied to players. + +## 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. +The event handler is any function that runs in your package that accepts certain expected parameters, which you can +expand with your own arguments. When it comes to the moment when the given command needs to be executed, it does so +providing that number of arguments it accepts is equal number of event arguments, and extra arguments passed when +defining the callback with `entity_event`. + +The following events can be handled by entities: + +* `'on_tick'`: executes every tick right before the entity is ticked in the game. Required arguments: `entity` +* `'on_move'`: executes every time an entity changes position, invoked just after it has been moved to the new position. Required arguments: `entity, velocity, pos1, pos2` +* `'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 attach an inapplicable event to it. + +In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + +### `entity_load_handler(descriptor / descriptors, function)`, `entity_load_handler(descriptor / descriptors, call_name, ... args?)` + +Attaches a callback to trigger when any entity matching the following type / types is loaded in the game, allowing to grab a handle +to an entity right when it is loaded to the world without querying them every tick. Callback expects two parameters - the entity, +and a boolean value indicating if the entity was newly created(`true`) or just loaded from disk. Single argument functions accepting +only entities are allowed, but deprecated and will be removed at some point. + +If callback is `null`, then the current entity handler, if present, is removed. Consecutive calls to `entity_load_handler` will add / subtract +of the currently targeted entity types pool. + +Like other global events, calls to `entity_load_handler` should only be attached in apps with global scope. For player scope apps, +it will be called multiple times, once for each player. That's likely not what you want to do. + +``` +// veryfast method of getting rid of all the zombies. Callback is so early, its packets haven't reached yet the clients +// so to save on log errors, removal of mobs needs to be scheduled for later. +entity_load_handler('zombie', _(e, new) -> schedule(0, _(outer(e)) -> modify(e, 'remove'))) + +// another way to do it is to remove the entity when it starts ticking +entity_load_handler('zombie', _(e, new) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'remove'))) + +// making all zombies immediately faster and less susceptible to friction of any sort +entity_load_handler('zombie', _(e, new) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'motion', 1.2*e~'motion'))) +``` + +Word of caution: entities can be loaded with chunks in various states, for instance when a chunk is being generated, this means +that accessing world blocks would cause the game to freeze due to force generating that chunk while generating the chunk. Make +sure to never assume the chunk is ready and use `entity_load_handler` to schedule actions around the loaded entity, +or manipulate entity directly. + +Also, it is possible that mobs that spawn with world generation, while being 'added' have their metadata serialized and cached +internally (vanilla limitation), so some modifications to these entities may have no effect on them. This affects mobs created with +world generation. + +For instance the following handler is safe, as it only accesses the entity directly. It makes all spawned pigmen jump +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, modify(e, 'motion', 0, 1, 0)) ) +``` +But the following handler, attempting to despawn pigmen that spawn in portals, will cause the game to freeze due to cascading access to blocks that would cause neighbouring chunks +to force generate, causing also error messages for all pigmen caused by packets send after entity is removed by script. +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new && block(pos(e))=='nether_portal', modify(e, 'remove') ) ) +``` +Easiest method to circumvent these issues is delay the check, which may or may not cause cascade load to happen, but +will definitely break the infinite chain. +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, schedule(0, _(outer(e)) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ) ) +``` +But the best is to perform the check first time the entity will be ticked - giving the game all the time to ensure chunk +is fully loaded and entity processing, removing the tick handler: +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, entity_event(e, 'on_tick', _(e) -> ( if(block(pos(e))=='nether_portal', modify(e, 'remove')); entity_event(e, 'on_tick', null) ) ) ) ) +``` +Looks little convoluted, but that's the safest method to ensure your app won't crash. + +### `entity_event(e, event, function)`, `entity_event(e, event, call_name, ... args?)` + +Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the +original required arguments for the event handler. + +
+protect_villager(entity, amount, source, source_entity, healing_player) ->
+(
+   if(source_entity && source_entity~'type' != 'player',
+      modify(entity, 'health', amount + entity~'health' );
+      particle('end_rod', pos(entity)+[0,3,0]);
+      print(str('%s healed thanks to %s', entity, healing_player))
+   )
+);
+__on_player_interacts_with_entity(player, entity, hand) ->
+(
+   if (entity~'type' == 'villager',
+      entity_event(entity, 'on_damage', 'protect_villager', player~'name')
+   )
+)
+
+ +In this case this will protect a villager from entity damage (zombies, etc.) except from players by granting all the +health back to the villager after being harmed. +# Inventory and Items API + +## Manipulating inventories of blocks and entities + +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, or can be preceded with inventory +type. +Inventory type can be `null` (default), `'enderchest'` denoting player enderchest storage, or `'equipment'` applying to +entities hand and armour pieces. Then the type can be followed by entity, block or position coordinates. +For instance, player enderchest inventory requires +two arguments, keyword `'enderchest'`, followed by the player entity argument, (or a single argument as a string of a +form: `'enderchest_steve'` for legacy support). If your player name starts with `'enderchest_'`, first of all, tough luck, +but then 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. + +A few living entities can have both: their regular inventory, and their equipment inventory. +Player's regular inventory already contains the equipment, but you can access the equipment part as well, as well as +their enderchest separately. For entity types that only have +their equipment inventory, the equipment is returned by default (`null` type). + +If that's confusing see examples under `inventory_size` on how to access inventories. All other `inventory_...()` functions +use the same scheme. + + + If the entity or a block doesn't have +an inventory, all API functions typically do nothing and return null. + +Most items returned are in the form of a triple of item name, count, and the full nbt of an item. When saving an item, if the +nbt is provided, it overrides the item type provided in the name. + +### `item_list(tag?)` + +With no arguments, returns a list of all items in the game. With an item tag provided, list items matching the tag, or `null` if tag is not valid. + +### `item_tags(item, tag?)` + +Returns list of tags the item belongs to, or, if tag is provided, `true` if an item matches the tag, `false` if it doesn't and `null` if that's not a valid tag + +Throws `unknown_item` if item doesn't exist. + +### `stack_limit(item)` + +Returns number indicating what is the stack limit for the item. Its typically 1 (non-stackable), 16 (like buckets), +or 64 - rest. It is recommended to consult this, as other inventory API functions ignore normal stack limits, and +it is up to the programmer to keep it at bay. As of 1.13, game checks for negative numbers and setting an item to +negative is the same as empty. + +Throws `unknown_item` if item doesn't exist. + +
+stack_limit('wooden_axe') => 1
+stack_limit('ender_pearl') => 16
+stack_limit('stone') => 64
+
+ +### `recipe_data(item, type?)`, `recipe_data(recipe, type?)` + +returns all recipes matching either an `item`, or represent actual `recipe` name. In vanilla datapack, for all items +that have one recipe available, the recipe name is the same as the item name but if an item has multiple recipes, its +direct name can be different. + +Recipe type can take one of the following options: + * `'crafting'` - default, crafting table recipe + * `'smelting'` - furnace recipe + * `'blasting'` - blast furnace recipe + * `'smoking'` - smoker recipe + * `'campfire_cooking'` - campfire recipe + * `'stonecutting'` - stonecutter recipe + * `'smithing'` - smithing table (1.16+) + + The return value is a list of available recipes (even if there is only one recipe available). Each recipe contains of + an item triple of the crafting result, list of ingredients, each containing a list of possible variants of the + ingredients in this slot, as item triples, or `null` if its a shaped recipe and a given slot in the patterns is left + empty, and recipe specification as another list. Possible recipe specs is: + * `['shaped', width, height]` - shaped crafting. `width` and `height` can be 1, 2 or 3. + * `['shapeless']` - shapeless crafting + * `['smelting', duration, xp]` - smelting/cooking recipes + * `['cutting']` - stonecutter recipe + * `['special']` - special crafting recipe, typically not present in the crafting menu + * `['custom']` - other recipe types + +Note that ingredients are specified as tripes, with count and nbt information. Currently all recipes require always one +of the ingredients, and for some recipes, even if the nbt data for the ingredient is specified (e.g. `dispenser`), it +can accept items of any tags. + +Also note that some recipes leave some products in the crafting window, and these can be determined using + `crafting_remaining_item()` function + + Examples: +
+ recipe_data('iron_ingot_from_nuggets')
+ recipe_data('iron_ingot')
+ recipe_data('glass', 'smelting')
+ 
+ +### `crafting_remaining_item(item)` + +returns `null` if the item has no remaining item in the crafting window when used as a crafting ingredient, or an +item name that serves as a replacement after crafting is done. Currently it can only be buckets and glass bottles. + +### `inventory_size(inventory)` + +Returns the size of the inventory for the entity or block in question. Returns null if the block or entity don't +have an inventory. + +
+inventory_size(player()) => 41
+inventory_size('enderchest', player()) => 27 // enderchest
+inventory_size('equipment', player()) => 6 // equipment
+inventory_size(null, player()) => 41  // default inventory for players
+
+inventory_size(x,y,z) => 27 // chest
+inventory_size(block(pos)) => 5 // hopper
+
+horse = spawn('horse', x, y, z);
+inventory_size(horse); => 2 // default horse inventory
+inventory_size('equipment', horse); => 6 // unused horse equipment inventory
+inventory_size(null, horse); => 2 // default horse
+
+creeper = spawn('creeper', x, y, z);
+inventory_size(creeper); => 6 // default creeper inventory is equipment since it has no other
+inventory_size('equipment', creeper); => 6 // unused horse equipment inventory
+inventory_size(null, creeper); => 6 // creeper default is its equipment
+
+ +### `inventory_has_items(inventory)` + +Returns true, if the inventory is not empty, false if it is empty, and null, if its not an inventory. + +
    inventory_has_items(player()) => true
+    inventory_has_items(x,y,z) => false // empty chest
+    inventory_has_items(block(pos)) => null // stone
+
+ +### `inventory_get(inventory, slot)` + +Returns the item in the corresponding inventory slot, or null if slot empty or inventory is invalid. You can use +negative numbers to indicate slots counted from 'the back'. + +
+inventory_get(player(), 0) => null // nothing in first hotbar slot
+inventory_get(x,y,z, 5) => ['stone', 1, {id:"minecraft:stone"}]
+inventory_get(player(), -1) => ['diamond_pickaxe', 1, {components:{"minecraft:damage":4},id:"minecraft:diamond_pickaxe"}] // slightly damaged diamond pick in the offhand
+
+ +### `inventory_set(inventory, slot, count, item?, nbt?)` + +Modifies or sets a stack in inventory. specify count 0 to empty the slot. If item is not specified, keeps existing +item, just modifies the count. If item is provided - replaces current item. If nbt is provided - uses the tag to create the item fully +ignoring the item name. If nbt is provided and count is not null, the sets the custom count on the tag from the count parameter. +If count is `null` and item is `null`, an item is entirely defined by the `nbt` parameter. Returns previous stack in that slot. + +
+inventory_set(player(), 0, 0) => ['stone', 64, {id:"minecraft:stone"}] // player had a stack of stone in first hotbar slot
+inventory_set(player(), 0, 6) => ['diamond', 64, {id:"minecraft:diamond"}] // changed stack of diamonds in player slot to 6
+inventory_set(player(), 0, 1, 'diamond_axe','{components:{"minecraft:damage":5},id:"minecraft:diamond_axe"}') => null //added slightly damaged diamond axe to first player slot
+inventory_set(player(), 0, null, null, '{components:{"minecraft:damage":5},id:"minecraft:diamond_axe"}') => null // same effect as above
+
+ +### `inventory_find(inventory, item, start_slot?, ), inventory_find(inventory, null, start_slot?)` + +Finds the first slot with a corresponding item in the inventory, or if queried with null: the first empty slot. +Returns slot number if found, or null otherwise. Optional start_slot argument allows to skip all preceeding slots +allowing for efficient (so not slot-by-slot) inventory search for items. + +
+inventory_find(player(), 'stone') => 0 // player has stone in first hotbar slot
+inventory_find(player(), null) => null // player's inventory has no empty spot
+while( (slot = inventory_find(p, 'diamond', slot)) != null, 41, drop_item(p, slot) )
+    // spits all diamonds from player inventory wherever they are
+inventory_drop(x,y,z, 0) => 64 // removed and spawned in the world a full stack of items
+
+ +Throws `unknown_item` if item doesn't exist. + +### `inventory_remove(inventory, item, amount?)` + +Removes amount (defaults to 1) of item from inventory. If the inventory doesn't have the defined amount, nothing +happens, otherwise the given amount of items is removed wherever they are in the inventory. Returns boolean +whether the removal operation was successful. Easiest way to remove a specific item from player inventory +without specifying the slot. + +
+inventory_remove(player(), 'diamond') => 1 // removed diamond from player inventory
+inventory_remove(player(), 'diamond', 100) => 0 // player doesn't have 100 diamonds, nothing happened
+
+ +### `drop_item(inventory, slot, amount?, )` + +Drops the items from indicated inventory slot, like player that Q's an item or villager, that exchanges food. +You can Q items from block inventories as well. default amount is 0 - which is all from the slot. +NOTE: hoppers are quick enough to pick all the queued items from their inventory anyways. +Returns size of the actual dropped items. + +
+inventory_drop(player(), 0, 1) => 1 // Q's one item on the ground
+inventory_drop(x,y,z, 0) => 64 // removed and spawned in the world a full stack of items
+
+ +## Screens + +A screen is a value type used to open screens for a player and interact with them. +For example, this includes the chest inventory gui, the crafting table gui and many more. + +### `create_screen(player, type, name, callback?)` + +Creates and opens a screen for a `player`. + +Available `type`s: + +* `anvil` +* `beacon` +* `blast_furnace` +* `brewing_stand` +* `cartography_table` +* `crafting` +* `enchantment` +* `furnace` +* `generic_3x3` +* `generic_9x1` +* `generic_9x2` +* `generic_9x3` +* `generic_9x4` +* `generic_9x5` +* `generic_9x6` +* `grindstone` +* `hopper` +* `lectern` +* `loom` +* `merchant` +* `shulker_box` +* `smithing` +* `smoker` +* `stonecutter` + +The `name` parameter can be a formatted text and will be displayed at the top of the screen. +Some screens like the lectern or beacon screen don't show it. + +Optionally, a `callback` function can be passed as the fourth argument. +This functions needs to have four parameters: +`_(screen, player, action, data) -> ...` + +The `screen` parameter is the screen value of the screen itself. +`player` is the player who interacted with the screen. +`action` is a string corresponding to the interaction type. +Can be any of the following: + +Slot interactions: + +* `pickup` +* `quick_move` +* `swap` +* `clone` +* `throw` +* `quick_craft` +* `pickup_all` + +The `data` for this interaction is a map, with a `slot` and `button` value. +`slot` is the slot index of the slot that was clicked. +When holding an item in the cursor stack and clicking inside the screen, +but not in a slot, this is -1. +If clicked outside the screen (where it would drop held items), this value is null. +The `button` is the mouse button used to click the slot. + +For the `swap` action, the `button` is the number key 0-8 for a certain hotbar slot. + +For the `quick_craft` action, the `data` also contains the `quick_craft_stage`, +which is either 0 (beginning of quick crafting), 1 (adding item to slot) or 2 (end of quick crafting). + +Other interactions: + +* `button` Pressing a button in certain screens that have button elements (enchantment table, lectern, loom and stonecutter) +The `data` provides a `button`, which is the index of the button that was pressed. +Note that for lecterns, this index can be certain a value above 100, for jumping to a certain page. +This can come from formatted text inside the book, with a `change_page` click event action. + +* `close` Triggers when the screen gets closed. No `data` provided. + +* `select_recipe` When clicking on a recipe in the recipe book. +`data` contains a `recipe`, which is the identifier of the clicked recipe, +as well as `craft_all`, which is a boolean specifying whether +shift was pressed when selecting the recipe. + +* `slot_update` Gets called **after** a slot has changed contents. `data` provides a `slot` and `stack`. + +By returning a string `'cancel'` in the callback function, +the screen interaction can be cancelled. +This doesn't work for the `close` action. + +The `create_screen` function returns a `screen` value, +which can be used in all inventory related functions to access the screens' slots. +The screen inventory covers all slots in the screen and the player inventory. +The last slot is the cursor stack of the screen, +meaning that using `-1` can be used to modify the stack the players' cursor is holding. + +### `close_screen(screen)` + +Closes the screen of the given screen value. +Returns `true` if the screen was closed. +If the screen is already closed, returns `false`. + +### `screen_property(screen, property)` + +### `screen_property(screen, property, value)` + +Queries or modifies a certain `property` of a `screen`. +The `property` is a string with the name of the property. +When called with `screen` and `property` parameter, returns the current value of the property. +When specifying a `value`, +the property will be assigned the new `value` and synced with the client. + +**Options for `property` string:** + +| `property` | Required screen type | Type | Description | +|---|---|---|---| +| `name` | **All** | text | The name of the screen, as specified in the `create_screen()` function. Can only be queried. | +| `open` | **All** | boolean | Returns `true` if the screen is open, `false` otherwise. Can only be queried. | +| `fuel_progress` | furnace/smoker/blast_furnace | number | Current value of the fuel indicator. | +| `max_fuel_progress` | furnace/smoker/blast_furnace | number | Maximum value for the full fuel indicator. | +| `cook_progress` | furnace/smoker/blast_furnace | number | Cooking progress indicator value. | +| `max_cook_progress` | furnace/smoker/blast_furnace | number | Maximum value for the cooking progress indicator. | +| `level_cost` | anvil | number | Displayed level cost for the anvil. | +| `page` | lectern | number | Opened page in the lectern screen. | +| `beacon_level` | beacon | number | The power level of the beacon screen. This affects how many effects under primary power are grayed out. Should be a value between 0-5. | +| `primary_effect` | beacon | number | The effect id of the primary effect. This changes the effect icon on the button on the secondary power side next to the regeneration effect. | +| `secondary_effect` | beacon | number | The effect id of the secondary effect. This seems to change nothing, but it exists. | +| `brew_time` | brewing_stand | number | The brewing time indicator value. This goes from 0 to 400. | +| `brewing_fuel` | brewing_stand | number | The fuel indicator progress. Values range between 0 to 20. | +| `enchantment_power_x` | enchantment | number | The level cost of the shown enchantment. Replace `x` with 1, 2 or 3 (e.g. `enchantment_power_2`) to target the first, second or third enchantment. | +| `enchantment_id_x` | enchantment | number | The id of the enchantment shown (replace `x` with the enchantment slot 1/2/3). | +| `enchantment_level_x` | enchantment | number | The enchantment level of the enchantment. | +| `enchantment_seed` | enchantment | number | The seed of the enchanting screen. This affects the text shown in the standard Galactic alphabet. | +| `banner_pattern` | loom | number | The selected banner pattern inside the loom. | +| `stonecutter_recipe` | stonecutter | number | The selected recipe in the stonecutter. | + +### Screen example scripts + +
+Chest click event + +```py +__command() -> ( + create_screen(player(),'generic_9x6',format('db Test'),_(screen, player, action, data) -> ( + print(player('all'),str('%s\n%s\n%s',player,action,data)); //for testing + if(action=='pickup', + inventory_set(screen,data:'slot',1,if(inventory_get(screen,data:'slot'),'air','red_stained_glass_pane')); + ); + 'cancel' + )); +); +``` +
+ +
+Anvil text prompt + +```py +// anvil text prompt gui +__command() -> ( + global_screen = create_screen(player(),'anvil',format('r Enter a text'),_(screen, player, action, data)->( + if(action == 'pickup' && data:'slot' == 2, + renamed_item = inventory_get(screen,2); + nbt = renamed_item:2; + name = parse_nbt(nbt:'display':'Name'):'text'; + if(!name, return('cancel')); //don't accept empty string + print(player,'Text: ' + name); + close_screen(screen); + ); + 'cancel' + )); + inventory_set(global_screen,0,1,'paper','{display:{Name:\'{"text":""}\'}}'); +); + +``` +
+ +
+Lectern flip book + +```py +// flip book lectern + +global_fac = 256/60; +curve(v) -> ( + v = v%360; + if(v<60,v*global_fac,v<180,255,v<240,255-(v-180)*global_fac,0); +); + +hex_from_hue(hue) -> str('#%02X%02X%02X',curve(hue+120),curve(hue),curve(hue+240)); + +make_char(hue) -> str('{"text":"▉","color":"%s"}',hex_from_hue(hue)); + +make_page(hue) -> ( + page = '['; + loop(15, //row + y = _; + loop(14, //col + x = _; + page += make_char(hue+x*4+y*4) + ','; + ); + ); + return(slice(page,0,-2)+']'); +); + + +__command() -> ( + screen = create_screen(player(),'lectern','Lectern example (this text is not visible)',_(screen, player, action, data)->( + if(action=='button', + print(player,'Button: ' + data:'button'); + ); + 'cancel' + )); + + page_count = 60; + pages = []; + + loop(page_count, + hue = _/page_count*360; + pages += make_page(hue); + ); + + nbt = encode_nbt({ + 'pages'-> pages, + 'author'->'-', + 'title'->'-', + 'resolved'->1 + }); + + inventory_set(screen,0,1,'written_book',nbt); + + task(_(outer(screen),outer(page_count))->( + while(screen != null && screen_property(screen,'open'),100000, + p = (p+1)%page_count; + screen_property(screen,'page',p); + sleep(50); + ); + )); +); + +``` +
+ +
+generic_3x3 cursor stack + +```py +__command() -> ( + screen = create_screen(player(),'generic_3x3','Title',_(screen, player, action, data) -> ( + if(action=='pickup', + // set slot to the cursor stack item + inventory_set(screen,data:'slot',1,inventory_get(screen,-1):0); + ); + 'cancel' + )); + + task(_(outer(screen))->( + // keep the cursor stack item blinking + while(screen_property(screen,'open'),100000, + inventory_set(screen,-1,1,'red_concrete'); + sleep(500); + inventory_set(screen,-1,1,'lime_concrete'); + sleep(500); + ); + )); +); +``` +
# Scarpet events system + +Scarpet provides the ability to execute specific function whenever an event occurs. The functions to be subscribed for an event +need to conform with the arguments to the event specification. There are several built-in events triggered when certain in-game +events occur, but app designers can create their own events and trigger them across all loaded apps. + +When loading the app, each function that starts +with `__on_` and has the required arguments, will be bound automatically to a corresponding built-in event. '`undef`'ying +of such function would result in unbinding the app from this event. Defining event hanlder via `__on_(... args) -> expr` is +equivalent of defining it via `handle_event('', _(... args) -> expr)` + +In case of `player` scoped apps, +all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, that don't have a specific +player target will be executed multiple times, once for each player app instance. While each player app instance is independent, +statically defined event handlers will be copied to each players app, but if you want to apply them in more controlled way, +defining event handlers for each player in `__on_start()` function is preferred. + +Most built-in events strive to report right before they take an effect in the game. The purpose of that is that this give a choice +for the programmer to handle them right away (as it happens, potentially affect the course of action by changing the +environment right before it), or decide to handle it after by scheduling another call for the end of the tick. Or both - +partially handle the event before it happens and handle the rest after. While in some cases this may lead to programmers +confusion (like handling the respawn event still referring to player's original position and dimension), but gives much +more control over these events. + +Some events also provide the ability to cancel minecraft's processing of the event by returning `'cancel'` from the event handler. +This only works for particular events that are triggered before they take an effect in the game. +However, cancelling the event will also stop events from subsequent apps from triggering. +The order of events being executed can be changed by specifying an `'event_priority'` in the app config, +with the highest value being executed first. +Note that cancelling some events might introduce a desynchronization to the client from the server, +creating ghost items or blocks. This can be solved by updating the inventory or block to the client, by using `inventory_set` or `set`. + +Programmers can also define their own events and signal other events, including built-in events, and across all loaded apps. + +## App scopes and event distribution + +Events triggered in an app can result in zero, one, or multiple executions, depending on the type of the event, and the app scope. + * player targeted events (like `player_breaks_block`) target each app once: + * for global scoped apps - targets a single app instance and provides `player` as the first argument. + * for player scoped apps - targets only a given player instance, providing player argument for API consistency, + since active player in player scoped apps can always be retrieved using `player()`. + * global events could be handled by multiple players multiple times (like `explosion`, or `tick`): + * for global scoped apps - triggered once for the single app instance. + * for player scoped apps - triggered N times for each player separately, so they can do something with that information + * custom player targeted events (using `signal_event(, , data)`): + * for global scoped apps - doesn't trigger at all, since there is no way to pass the required player. + To target global apps with player information, use `null` for player target, and add player information to the `data` + * for player scoped apps - triggers once for the specified player and its app instance + * custom general events (using `signal_event(, null, data)`) behave same like built-in global events: + * for global scoped apps - triggers once for the only global instance + * for player scoped apps - triggers N times, once for each player app instance + +## Built-in events + +Here is the list of events that are handled by default in scarpet. This list includes prefixes for function names, allowing apps +to register them when the app starts, but you can always add any handler function to any event using `/script event` command, +if it accepts the required number of parameters for the event. + +## Meta-events + +These events are not controlled / triggered by the game per se, but are important for the flow of the apps, however for all +intent and purpose can be treated as regular events. Unlike regular events, they cannot be hooked up to with `handle_event()`, +and the apps themselves need to have them defined as distinct function definitions. They also cannot be triggered via `signal_event()`. + +### `__on_start()` +Called once per app in its logical execution run. For `'global'` scope apps its executed right after the app is loaded. For +`'player'` scope apps, it is triggered once per player before the app can be used by that player. Since each player app acts +independently from other player apps, this is probably the best location to include some player specific initializations. Static +code (i.e. code typed directly in the app code that executes immediately, outside of function definitions), will only execute once +per app, regardless of scope, `'__on_start()'` allows to reliably call player specific initializations. However, most event handlers +defined in the static body of the app will be copied over to each player scoped instance when they join. + +### `__on_close()` + +Called once per app when the app is closing or reloading, right before the app is removed. +For player scoped apps, its called once per player. Scarpet app engine will attempt to call `'__on_close()'` even if +the system is closing down exceptionally. + + +## Built-in global events + +Global events will be handled once per app that is with `'global'` scope. With `player` scoped apps, each player instance + will be triggered once for each player, so a global event may be executed multiple times for such apps. + +### `__on_server_starts()` +Event triggers after world is loaded and after all startup apps have started. It won't be triggered with `/reload`. + +### `__on_server_shuts_down()` +Event triggers when the server started the shutdown process, before `__on_close()` is executed. Unlike `__on_close()`, it doesn't +trigger with `/reload`. + +### `__on_tick()` +Event triggers at the beginning of each tick, located in the overworld. You can use `in_dimension()` +to access other dimensions from there. + +### `__on_tick_nether()` (Deprecated) +Duplicate of `tick`, just automatically located in the nether. Use `__on_tick() -> in_dimension('nether', ... ` instead. + +### `__on_tick_ender()` (Deprecated) +Duplicate of `tick`, just automatically located in the end. Use `__on_tick() -> in_dimension('end', ... ` instead. + +### `__on_chunk_generated(x, z)` +Called right after a chunk at a given coordinate is full generated. `x` and `z` correspond +to the lowest x and z coords in the chunk. Handling of this event is scheduled as an off-tick task happening after the +chunk is confirmed to be generated and loaded to the game, due to the off-thread chunk loading in the game. So +handling of this event is not technically guaranteed if the game crashes while players are moving for example, and the game +decides to shut down after chunk is fully loaded and before its handler is processed in between ticks. In normal operation +this should not happen, but let you be warned. + +### `__on_chunk_loaded(x, z)` +Called right after a chunk at a given coordinate is loaded. All newly generated chunks are considered loaded as well. + `x` and `z` correspond to the lowest x and z coordinates in the chunk. + +### `__on_chunk_unloaded(x, z)` +Called right before a chunk at the given coordinates is unloaded. `x` and `z` correspond to the lowest x and z coordinates in the chunk. + +### `__on_lightning(block, mode)` +Triggered right after a lightning strikes. Lightning entity as well as potential horseman trap would +already be spawned at that point. `mode` is `true` if the lightning did cause a trap to spawn. + +### `__on_explosion(pos, power, source, causer, mode, fire)` + +Event triggered right before explosion takes place and before has any effect on the world. `source` can be an entity causing +the explosion, and `causer` the entity triggering it, +`mode` indicates block effects: `'none'`, `'break'` (drop all blocks), or `'destroy'` - drop few blocks. Event +is not captured when `create_explosion()` is called. + +### `__on_explosion_outcome(pos, power, source, causer, mode, fire, blocks, entities)` +Triggered during the explosion, before any changes to the blocks are done, +but the decision to blow up is already made and entities are already affected. +The parameter `blocks` contains the list of blocks that will blow up (empty if `explosionNoBlockDamage` is set to `true`). +The parameter `entities` contains the list of entities that have been affected by the explosion. Triggered even with `create_explosion()`. + +### `__on_carpet_rule_changes(rule, new_value)` +Triggered when a carpet mod rule is changed. It includes extension rules, not using default `/carpet` command, +which will then be namespaced as `namespace:rule`. + +### Entity load event -> check in details on `entity_load_handler()` + +These will trigger every time an entity of a given type is loaded into the game: spawned, added with a chunks, +spawned from commands, anything really. Check `entity_load_handler()` in the entity section for details. + +## Built-in player events + +These are triggered with a player context. For apps with a `'player'` scope, they trigger once for the appropriate +player. In apps with `global` scope they trigger once as well as a global event. + +### `__on_player_uses_item(player, item_tuple, hand)` +Triggers with a right click action. Event is triggered right after a server receives the packet, before the +game manages to do anything about it. Event triggers when player starts eating food, or starts drawing a bow. +Use `player_finishes_using_item`, or `player_releases_item` to capture the end of these events. + +This event can be cancelled by returning `'cancel'`, which prevents the item from being used. + +Event is not triggered when a player places a block, for that use +`player_right_clicks_block` or `player_places_block` event. + +### `__on_player_releases_item(player, item_tuple, hand)` +Player stops right-click-holding on an item that can be held. This event is a result of a client request. +Example events that may cause it to happen is releasing a bow. The event is triggered after the game processes +the request, however the `item_tuple` is provided representing the item that the player started with. You can use that and +compare with the currently held item for a delta. + +### `__on_player_finishes_using_item(player, item_tuple, hand)` +Player using of an item is done. This is controlled server side and is responsible for such events as finishing +eating. The event is triggered after confirming that the action is valid, and sending the feedback back +to the client, but before triggering it and its effects in game. + +This event can be cancelled by returning `'cancel'`, which prevents the player from finishing using the item. + +### `__on_player_clicks_block(player, block, face)` +Representing left-click attack on a block, usually signifying start of breaking of a block. Triggers right after the server +receives a client packet, before anything happens on the server side. + +This event can be cancelled by returning `'cancel'`, which stops the player from breaking a block. + + +### `__on_player_breaks_block(player, block)` +Called when player breaks a block, right before any changes to the world are done, but the decision is made to remove the block. + +This event can be cancelled by returning `'cancel'`, which prevents the block from being placed. + +### `__on_player_right_clicks_block(player, item_tuple, hand, block, face, hitvec)` +Called when player right clicks on a block with anything, or interacts with a block. This event is triggered right +before other interaction events, like `'player_interacts_with_block'` or `'player_places_block'`. + +This event can be cancelled by returning `'cancel'`, which prevents the player interaction. + +### `__on_player_interacts_with_block(player, hand, block, face, hitvec)` +Called when player successfully interacted with a block, which resulted in activation of said block, +right after this happened. + +### `__on_player_placing_block(player, item_tuple, hand, block)` +Triggered when player places a block, before block is placed in the world. + +This event can be cancelled by returning `'cancel'`, which prevents the block from being placed. + +### `__on_player_places_block(player, item_tuple, hand, block)` +Triggered when player places a block, after block is placed in the world, but before scoreboard is triggered or player inventory +adjusted. + +### `__on_player_interacts_with_entity(player, entity, hand)` +Triggered when player right clicks (interacts) with an entity, even if the entity has no vanilla interaction with the player or +the item they are holding. The event is invoked after receiving a packet from the client, before anything happens server side +with that interaction. + +This event can be cancelled by returning `'cancel'`, which prevents the player interacting with the entity. + +### `__on_player_trades(player, entity, buy_left, buy_right, sell)` +Triggered when player trades with a merchant. The event is invoked after the server allow the trade, but before the inventory +changes and merchant updates its trade-uses counter. +The parameter `entity` can be `null` if the merchant is not an entity. + +### `__on_player_collides_with_entity(player, entity)` +Triggered every time a player - entity collisions are calculated, before effects of collisions are applied in the game. +Useful not only when colliding with living entities, but also to intercept items or XP orbs before they have an effect +on the player. + +### `__on_player_chooses_recipe(player, recipe, full_stack)` +Triggered when a player clicks a recipe in the crafting window from the crafting book, after server received +a client request, but before any items are moved from its inventory to the crafting menu. + +This event can be cancelled by returning `'cancel'`, which prevents the recipe from being moved into the crafting grid. + +### `__on_player_switches_slot(player, from, to)` +Triggered when a player changes their selected hotbar slot. Applied right after the server receives the message to switch +the slot. + +### `__on_player_swaps_hands(player)` +Triggered when a player sends a command to swap their offhand item. Executed before the effect is applied on the server. + +This event can be cancelled by returning `'cancel'`, which prevents the hands from being swapped. + +### `__on_player_swings_hand(player, hand)` +Triggered when a player starts swinging their hand. The event typically triggers after a corresponding event that caused it +(`player_uses_item`, `player_breaks_block`, etc.), but it triggers also after some failed events, like attacking the air. When +swinging continues as an effect of an action, no new swinging events will be issued until the swinging is stopped. + +### `__on_player_attacks_entity(player, entity)` +Triggered when a player attacks entity, right before it happens server side. + +This event can be cancelled by returning `'cancel'`, which prevents the player from attacking the entity. + +### `__on_player_takes_damage(player, amount, source, source_entity)` +Triggered when a player is taking damage. Event is executed right after potential absorption was applied and before +the actual damage is applied to the player. + +This event can be cancelled by returning `'cancel'`, which prevents the player from taking damage. + +### `__on_player_deals_damage(player, amount, entity)` +Triggered when a player deals damage to another entity. Its applied in the same moment as `player_takes_damage` if both +sides of the event are players, and similar for all other entities, just their absorption is taken twice, just noone ever +notices that ¯\_(ツ)_/¯ + +This event can be cancelled by returning `'cancel'`, which prevents the damage from being dealt. + +### `__on_player_dies(player)` +Triggered when a player dies. Player is already dead, so don't revive them then. Event applied before broadcasting messages +about players death and applying external effects (like mob anger etc). + +### `__on_player_respawns(player)` +Triggered when a player respawns. This includes spawning after death, or landing in the overworld after leaving the end. +When the event is handled, a player is still in its previous location and dimension - will be repositioned right after. In +case player died, its previous inventory as already been scattered, and its current inventory will not be copied to the respawned +entity, so any manipulation to player data is +best to be scheduled at the end of the tick, but you can still use its current reference to query its status as of the respawn event. + +### `__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension)` +Called when a player moves from one dimension to another. Event is handled still when the player is in its previous +dimension and position. + +`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 controlled by the teleport, or a player can still see the end credits. After + the player is eligible to respawn in the overworld, `player_respawns` will be triggered. + +### `__on_player_rides(player, forward, strafe, jumping, sneaking)` +Triggers when a server receives movement controls when riding vehicles. Its handled before the effects are applied +server side. + +### `__on_player_jumps(player)` +Triggered when a game receives a jump input from the client, and the player is considered standing on the ground. + + +### `__on_player_deploys_elytra(player)` +Triggered when a server receives a request to deploy elytra, regardless if the flight was agreed upon server side.. + +### `__on_player_wakes_up(player)` +Player wakes up from the bed mid sleep, but not when it is kicked out of bed because it finished sleeping. + +### `__on_player_escapes_sleep(player)` +Same as `player_wakes_up` but only triggered when pressing the ESC button. Not sure why Mojang decided to send that event +twice when pressing escape, but might be interesting to be able to detect that. + +### `__on_player_starts_sneaking(player)` +### `__on_player_stops_sneaking(player)` +### `__on_player_starts_sprinting(player)` +### `__on_player_stops_sprinting(player)` +Four events triggered when player controls for sneaking and sprinting toggle. + +### `__on_player_drops_item(player)` +### `__on_player_drops_stack(player)` +Triggered when the game receives the request from a player to drop one item or full stack from its inventory. +Event happens before anything is changed server side. + +These events can be cancelled by returning `'cancel'`, which prevents the player dropping the items. + +### `__on_player_picks_up_item(player, item)` +Triggered AFTER a player successfully ingested an item in its inventory. Item represents the total stack of items +ingested by the player. The exact position of these items is unknown as technically these +items could be spread all across the inventory. + +### `__on_player_connects(player)` +Triggered when the player has successfully logged in and was placed in the game. + +### `__on_player_disconnects(player, reason)` +Triggered when a player sends a disconnect package or is forcefully disconnected from the server. + +### `__on_player_message(player, message)` +Triggered when a player sends a chat message. + +### `__on_player_command(player, command)` +Triggered when a player runs a command. Command value is returned without the / in front. + +This event can be cancelled by returning `'cancel'`, which prevents the message from being sent. + +### `__on_statistic(player, category, event, value)` +Triggered when a player statistic changes. Doesn't notify on periodic an rhythmic events, i.e. +`time_since_death`, `time_since_rest`, and `played_one_minute` since these are triggered every tick. Event +is handled before scoreboard values for these statistics are changed. + +## Custom events and hacking into scarpet event system + +App programmers can define and trigger their own custom events. Unlike built-in events, all custom events pass a single value +as an argument, but this doesn't mean that they cannot pass a complex list, map, or nbt tag as a message. Each event signal is +either targeting all apps instances for all players, including global apps, if no target player has been identified, +or only player scoped apps, if the target player +is specified, running once for that player app. You cannot target global apps with player-targeted signals. Built-in events +do target global apps, since their first argument is clearly defined and passed. That may change in the future in case there is +a compelling argument to be able to target global apps with player scopes. + +Programmers can also handle built-in events the same way as custom events, as well as triggering built-in events, which I have +have no idea why you would need that. The following snippets have the same effect: + +``` +__on_player_breaks_block(player, block) -> print(player+' broke '+block); +``` +and +``` +handle_event('player_breaks_block', _(player, block) -> print(player+' broke '+block)); +``` + +as well as +``` +undef('__on_player_breaks_block'); +``` +and +``` +handle_event('player_breaks_block', null); +``` +And `signal_event` can be used as a trigger, called twice for player based built-in events +``` +signal_event('player_breaks_block', player, player, block); // to target all player scoped apps +signal_event('player_breaks_block', null , player, block); // to target all global scoped apps and all player instances +``` +or (for global events) +``` +signal_event('tick') // trigger all apps with a tick event +``` + +### `handle_event(event, callback ...)` + +Provides a handler for an event identified by the '`event`' argument. If the event doesn't exist yet, it will be created. +All loaded apps globally can trigger that event, when they call corresponding `signal_event(event, ...)`. Callback can be +defined as a function name, function value (or a lambda function), along with optional extra arguments that will be passed +to it when the event is triggered. All custom events expect a function that takes one free argument, passed by the event trigger. +If extra arguments are provided, they will be appended to the argument list of the callback function. + +Returns `true` if subscription to the event was successful, or `false` if it failed (for instance wrong scope for built-in event, +or incorrect number of parameters for the event). + +If a callback is specified as `null`, the given app (or player app instance )stops handling that event. + +
+foo(a) -> print(a);
+handle_event('boohoo', 'foo');
+
+bar(a, b, c) -> print([a, b, c]);
+handle_event('boohoo', 'bar', 2, 3) // using b = 2, c = 3, a - passed by the caller
+
+handle_event('tick', _() -> foo('tick happened')); // built-in event
+
+handle_event('tick', null)  // nah, ima good, kthxbai
+
+ +In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + +### `signal_event(event, target_player?, ... args?)` + +Fires a specific event. If the event does not exist (only `handle_event` creates missing new events), or provided argument list +was not matching the callee expected arguments, returns `null`, +otherwise returns number of apps notified. If `target_player` is specified and not `null` triggers a player specific event, targeting +only `player` scoped apps for that player. Apps with globals scope will not be notified even if they handle this event. +If the `target_player` is omitted or `null`, it will target `global` scoped apps and all instances of `player` scoped apps. +Note that all built-in player events have a player as a first argument, so to trigger these events, you need to +provide them twice - once to specify the target player scope and second - to provide as an argument to the handler function. + +
+signal_event('player_breaks_block', player, player, block); // to target all player scoped apps
+signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps and all player instances
+signal_event('tick') // trigger all apps with a tick event
+
+ +## Custom events example + +The following example shows how you can communicate between different instances of the same player scoped app. It important to note +that signals can trigger other apps as well, assuming the name of the event matches. In this case the request name is called +`tp_request` and is triggered with a command. + + +``` +// tpa.sc +global_requester = null; +__config() -> { + 'commands' -> { + '' -> _(to) -> signal_event('tp_request', to, player()), + 'accept' -> _() -> if(global_requester, + run('tp '+global_requester~'command_name'); + global_requester = null + ) + }, + 'arguments' -> { + 'player' -> {'type' -> 'players', 'single' -> true} + } +}; +handle_event('tp_request', _(req) -> ( + global_requester = req; + print(player(), format( + 'w '+req+' requested to teleport to you. Click ', + 'yb here', '^yb here', '!/tpa accept', + 'w to accept it.' + )); +)); +``` + +## `/script event` command + +used to display current events and bounded functions. use `add_to` to register a new event, or `remove_from` to +unbind a specific function from an event. Function to be bounded to an event needs to have the same number of +parameters as the action is attempting to bind to (see list above). All calls in modules loaded via `/script load` +that handle specific built-in events will be automatically bounded, and unbounded when script is unloaded. +# Scoreboard + +### `scoreboard()`, `scoreboard(objective)`, `scoreboard(objective, key)`, `scoreboard(objective, key, value)` + +Displays or modifies individual scoreboard values. With no arguments, returns the list of current objectives. +With specified `objective`, lists all keys (players) associated with current objective, or `null` if objective does not exist. +With specified `objective` and +`key`, returns current value of the objective for a given player (key). With additional `value` sets a new scoreboard + value, returning previous value associated with the `key`. If the `value` is null, resets the scoreboard value. + +### `scoreboard_add(objective, criterion?)` + +Adds a new objective to scoreboard. If `criterion` is not specified, assumes `'dummy'`. +Returns `true` if the objective was created, or `null` if an objective with the specified name already exists. + +Throws `unknown_criterion` if criterion doesn't exist. + +
+scoreboard_add('counter')
+scoreboard_add('lvl','level')
+
+ +### `scoreboard_remove(objective)` `scoreboard_remove(objective, key)` + +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)` + +Sets display location for a specified `objective`. If `objective` is `null`, then display is cleared. If objective is invalid, +returns `null`. + +### `scoreboard_property(objective, property)` `scoreboard_property(objective, property, value)` + +Reads a property of an `objective` or sets it to a `value` if specified. Available properties are: + +* `criterion` +* `display_name` (Formatted text supported) +* `display_slot`: When reading, returns a list of slots this objective is displayed in, when modifying, displays the objective in the specified slot +* `render_type`: Either `'integer'` or `'hearts'`, defaults to `'integer'` if invalid value specified + +# Team + +### `team_list()`, `team_list(team)` + +Returns all available teams as a list with no arguments. + +When a `team` is specified, it returns all the players inside that team. If the `team` is invalid, returns `null`. + +### `team_add(team)`, `team_add(team,player)` + +With one argument, creates a new `team` and returns its name if successful, or `null` if team already exists. + + +`team_add('admin')` -> Create a team with the name 'admin' +`team_add('admin','Steve')` -> Joing the player 'Steve' into the team 'admin' + +If a `player` is specified, the player will join the given `team`. Returns `true` if player joined the team, or `false` if nothing changed since the player was already in this team. If the team is invalid, returns `null` + +### `team_remove(team)` + +Removes a `team`. Returns `true` if the team was deleted, or `null` if the team is invalid. + +### `team_leave(player)` + +Removes the `player` from the team he is in. Returns `true` if the player left a team, otherwise `false`. + +`team_leave('Steve')` -> Removes Steve from the team he is currently in +`for(team_list('admin'), team_leave('admin', _))` -> Remove all players from team 'admin' + +### `team_property(team,property,value?)` + +Reads the `property` of the `team` if no `value` is specified. If a `value` is added as a third argument, it sets the `property` to that `value`. + +* `collisionRule` + * Type: String + * Options: always, never, pushOtherTeams, pushOwnTeam + +* `color` + * Type: String + * Options: See [team command](https://minecraft.wiki/w/Commands/team#Arguments) (same strings as `'teamcolor'` [command argument](https://github.com/gnembon/fabric-carpet/blob/master/docs/scarpet/Full.md#command-argument-types) options) + +* `displayName` + * Type: String or FormattedText, when querying returns FormattedText + +* `prefix` + * Type: String or FormattedText, when querying returns FormattedText + +* `suffix` + * Type: String or FormattedText, when querying returns FormattedText + +* `friendlyFire` + * Type: boolean + +* `seeFriendlyInvisibles` + * Type: boolean + +* `nametagVisibility` + * Type: String + * Options: always, never, hideForOtherTeams, hideForOwnTeam + +* `deathMessageVisibility` + * Type: String + * Options: always, never, hideForOtherTeams, hideForOwnTeam + +Examples: + +``` +team_property('admin','color','dark_red') Make the team color for team 'admin' dark red +team_property('admin','prefix',format('r Admin | ')) Set prefix of all players in 'admin' +team_property('admin','display_name','Administrators') Set display name for team 'admin' +team_property('admin','seeFriendlyInvisibles',true) Make all players in 'admin' see other admins even when invisible +team_property('admin','deathMessageVisibility','hideForOtherTeams') Make all players in 'admin' see other admins even when invisible +``` + +## `bossbar()`, `bossbar(id)`, `bossbar(id,property,value?)` + +Manage bossbars just like with the `/bossbar` command. + +Without any arguments, returns a list of all bossbars. + +When an id is specified, creates a bossbar with that `id` and returns the id of the created bossbar. +Bossbar ids need a namespace and a name. If no namespace is specified, it will automatically use `minecraft:`. +In that case you should keep track of the bossbar with the id that `bossbar(id)` returns, because a namespace may be added automatically. +If the id was invalid (for example by having more than one colon), returns `null`. +If the bossbar already exists, returns `false`. + +`bossbar('timer') => 'minecraft:timer'` (Adds the namespace `minecraft:` because none is specified) + +`bossbar('scarpet:test') => 'scarpet:test'` In this case there is already a namespace specified + +`bossbar('foo:bar:baz') => null` Invalid identifier + +`bossbar(id,property)` is used to query the `property` of a bossbar. + +`bossbar(id,property,value)` can modify the `property` of the bossbar to a specified `value`. + +Available properties are: + +* color: can be `'pink'`, `'blue'`, `'red'`, `'green'`, `'yellow'`, `'purple'` or `'white'` + +* style: can be `'progress'`, `'notched_6'`, `'notched_10'`, `'notched_12'` or `'notched_20'` + +* value: value of the bossbar progress + +* max: maximum value of the bossbar progress, by default 100 + +* name: Text to display above the bossbar, supports formatted text + +* visible: whether the bossbar is visible or not + +* players: List of players that can see the bossbar + +* add_player: add a player to the players that can see this bossbar, this can only be used for modifying (`value` must be present) + +* remove: remove this bossbar, no `value` required + +``` +bossbar('script:test','style','notched_12') +bossbar('script:test','value',74) +bossbar('script:test','name',format('rb Test')) -> Change text +bossbar('script:test','visible',false) -> removes visibility, but keeps players +bossbar('script:test','players',player('all')) -> Visible for all players +bossbar('script:test','players',player('Steve')) -> Visible for Steve only +bossbar('script:test','players',null) -> Invalid player, removing all players +bossbar('script:test','add_player',player('Alex')) -> Add Alex to the list of players that can see the bossbar +bossbar('script:test','remove') -> remove bossbar 'script:test' +for(bossbar(),bossbar(_,'remove')) -> remove all bossbars +``` + + + + +# Auxiliary aspects + +Collection of other methods that control smaller, yet still important aspects of the game + +## Sounds + +### `sound()`, `sound(name, pos, volume?, pitch?, mixer?)` + +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 three numbers. Uses the same options as a + corresponding `playsound` command. + +Used with no arguments, returns a list of available sound names. Note that this list may not include all sounds that +clients will actually be able to receive (they may have more available via resourcepacks for example). + +## Particles + +### `particle()`, `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'`. + +Used with no arguments, return the list of available particle names. Note that some of the names do not correspond to a valid +particle that can be fed to `particle(...)` function due to a fact that some particles need more configuration +to be valid, like `dust`, `block` etc. Should be used as a reference only. + +Throws `unknown_particle` if particle doesn't exist. + +### `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. If a player (or player name) is supplied, only +that player will receive particles. + +Throws `unknown_particle` if particle doesn't exist. + +### `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. + +Throws `unknown_particle` if particle doesn't exist. + +## Markers + +### `draw_shape(shape, duration, key?, value?, ... )`, +### `draw_shape(shape, duration, [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. The drawback of sending shapes is batches is that they need to address +the same list of players, i.e. if multiple players from the list target different players, all shapes will be sent to all of them. + +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, or a list of players. If specified, the shapes will appear only for the specified + players (regardless where they are), otherwise it will be send to all players in the current dimension. + * `line` - (Deprecated) line thickness, defaults to 2.0pt. Not supported in 1.17's 3.2 core GL renderer. + * `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. Preceeding an axis + with `d`, like `dxdydz` would make so that entity position is treated discretely (rounded down). + * `debug` - if True, it will only be visible when F3+B entity bounding boxes is enabled. + * `facing` - applicable only to `'text'`, `'block'` or '`item'` shapes, where its facing. Possible options are: + * `player`: Default. Element always rotates to face the player eye position, + * `camera`: Element is placed on the plane orthogonal to player look vector, + * `north`, `south`, `east`, `west`, `up`, `down`: obvious + +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'`. + + * `'polygon'`: + * Required attributes: + * `points` - list of points defining vertices of the polygon + * Optional attributes: + * `relative` - list of bools. vertices of the polygon that affected by 'follow'. Could be a single bools to affact allpoints too. Default means that every point is affacted. + * `mode` - how those points are connected. may be "polygon"(default),"strip" or "triangles". "polygon" means that it will be viewed as vertices of a polygon center on the first one. "strip" means that it will be viewed as a triangles strip. "triangles" means that it will be viewed as some triangles that are not related to each other (therefor length of `points` in this mode have to be a multiple of 3). + * `inner` - if `true` it will make the inner edges be drawn as well. + * `doublesided` - if `true` it will make the shapes visible from the back as well. Default is `true`. + + * `'label'` - draws a text in the world. Default `line` attribute controls main font color. + `fill` controls the color of the background. + * Required attributes: + * `pos` - position + * `text` - string or formatted text to display + * Optional attributes + * `value` - string or formatted text to display instead of the main `text`. `value` unlike `text` + is not used to determine uniqueness of the drawn text so can be used to + display smoothly dynamic elements where value of an element is constantly + changing and updates to it are being sent from the server. + * `size` - float. Default font size is 10. + * `doublesided` - if `true` it will make the text visible from the back as well. Default is `false` (1.16+) + * `align` - text alignment with regards to `pos`. Default is `center` (displayed text is + centered with respect to `pos`), `left` (`pos` indicates beginning of text), and `right` (`pos` + indicates the end of text). + * `tilt`, `lean`, `turn` - additional rotations of the text on the canvas along all three axis + * `indent`, `height`, `raise` - offsets for text rendering on X (`indent`), Y (`height`), and Z axis (`raise`) + with regards to the plane of the text. One unit of these corresponds to 1 line spacing, which + can be used to display multiple lines of text bound to the same `pos` + + * `'block'`: draws a block at the specified position: + * Required attributes: + * `pos` - position of the object. + * `block` - the object to show. It is a block value or a name of a block with optional NBT data. + * Optional attributes: + * `tilt`, `lean`, `turn` - additional rotations along all three axis. It uses the block center as the origin. + * `scale` - scale of it in 3 axis-direction. should be a number or a list of 3 numbers (x,y,z). + * `skylight`, `blocklight` - light level. omit it to use local light level. should between 0~15. + + * `'item'`: draws an item at the specified position: + * Required attributes: + * `pos` - position of the object. + * `item` - the object to show. It is an item tuple or a string identified item that may have NBT data. + * Optional attributes: + * `tilt`, `lean`, `turn` - additional rotations along all three axis. for `block`, it use its block center as the origin. + * `scale` - scale of it in 3 axis-direction. should be a number or a list of 3 numbers (x,y,z). + * `skylight`, `blocklight` - light level. omit it to use local light level. should between 0~15. + * `variant` - one of `'none'`, `'thirdperson_lefthand'`, `'thirdperson_righthand'`, `'firstperson_lefthand'`, + `'firstperson_righthand'`, `'head'`, `'gui'`, `'ground'`, `'fixed'`. In addition to the literal meaning, + it can also be used to use special models of tridents and telescopes. + This attribute is experimental and use of it will change in the future. + + +### `create_marker(text, pos, rotation?, block?, interactive?)` + +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. + +If `interactive` (`true` by default) is `false`, the armorstand will be a marker and would not be interactive in any +gamemode. But blocks can be placed inside markers and will not catch any interaction events. + +Y Position of a marker text or block will be adjusted to make blocks or text appear at the specified position. +This makes so that actual armorstand position may be offset on Y axis. You would need to adjust your entity +locations if you plan to move the armorstand around after the fact. If both text and block are specified - one of them +will be aligned (armorstand type markers text shows up at their feet, while for regular armorstands - above the head, +while block on the head always render in the same position regardless if its a marker or not). + + +### `remove_all_markers()` + +Removes all scarpet markers from the loaded portion of the world created by this app, in case you didn't want to do + the proper cleanup. + +## System function + +### `nbt(expr)` + +Treats the argument as a nbt serializable string and returns its nbt value. In case nbt is not in a correct nbt +compound tag format, it will return `null` value. + +Consult section about container operations in `Expression` to learn about possible operations on nbt values. + +### `escape_nbt(expr)` + +Excapes all the special characters in the string or nbt tag and returns a string that can be stored in nbt directly +as a string value. + +### `tag_matches(daddy_tag, baby_tag, match_lists?)` + +Utility returning `true` if `baby_tag` is fully contained in `daddy_tag`. Anything matches `null` baby tag, and +Nothing is contained in a `null` daddy tag. If `match_lists` is specified and `false`, content of nested lists is ignored. +Default behaviour is to match them. + +### `parse_nbt(tag)` + +Converts NBT tag to a scarpet value, which you can navigate through much better. + +Converts: + - Compound tags into maps with string keys + - List tags into list values + - Numbers (Ints, Floats, Doubles, Longs) into a number + - Rest is converted to strings. + +### `encode_nbt(expr, force?)` + +Encodes value of the expression as an NBT tag. By default (or when `force` is false), it will only allow +to encode values that are guaranteed to return the same value when applied the resulting tag to `parse_nbt()`. +Supported types that can reliably convert back and forth to and from NBT values are: + - Maps with string keywords + - Lists of items of the same type (scarpet will take care of unifying value types if possible) + - Numbers (encoded as Ints -> Longs -> Doubles, as needed) + - Strings + +Other value types will only be converted to tags (including NBT tags) if `force` is true. They would require +extra treatment when loading them back from NBT, but using `force` true will always produce output / never +produce an exception. + +### `print(expr)`, `print(player/player_list, expr)` + +Displays the result of the expression to the chat. Overrides default `scarpet` behaviour of sending everything to stderr. +For player scoped apps it always by default targets the player for whom the app runs on behalf. +Can optionally define player or list of players to send the message to. + +### `format(components, ...)`, `format([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. + * `'@'` - a URL that will be opened when the text below it is clicked. + * `'&'` - a text that will be copied to clipboard 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
+
+ +### `item_display_name(item)` + Returns the name of the item as a Text Value. `item` should be a list of `[item_name, count, nbt]`, or just an item name. + + Please note that it is a translated value. treating it like a string (eg.slicing, breaking, changing its case) will turn it back into a normal string without translatable properties. just like a colorful formatted text loose its color. And the result of it converting to a string will use en-us (in a server) or your single player's language, but when you use print() or others functions that accept a text value to broadcast it to players, it will use each player's own language. + + If the item is renamed, it will also be reflected in the results. + + +### `display_title(players, type, text?, fadeInTicks?, stayTicks?, fadeOutTicks),` + +Sends the player (or players if `players` is a list) a title of a specific type, with optionally some times. + * `players` is either an online player or a list of players. When sending a single player, it will throw if the player is invalid or offline. + * `type` is either `'title'`, `'subtitle'`, `actionbar` or `clear`. + Note: `subtitle` will only be displayed if there is a title being displayed (can be an empty one) + * `title` is what title to send to the player. It is required except for `clear` type. Can be a text formatted using `format()` + * `...Ticks` are the number of ticks the title will stay in that state. + If not specified, it will use current defaults (those defaults may have changed from a previous `/title times` execution). + Executing with those will set the times to the specified ones. + Note that `actionbar` type doesn't support changing times (vanilla bug, see [MC-106167](https://bugs.mojang.com/browse/MC-106167)). + +### `display_title(players, 'player_list_header', text)` +### `display_title(players, 'player_list_footer', text)` + +Changes the header or footer of the player list for the specified targets. +If `text` is `null` or an empty string it will remove the header or footer for the specified targets. +In case the player has Carpet loggers running, the footer specified by Scarpet will appear above the loggers. + +### `logger(msg), logger(type, msg)` + +Prints the message to system logs, and not to chat. +By default prints an info, unless you specify otherwise in the `type` parameter. + +Available output types: + +`'debug'`, `'warn'`, `'fatal'`, `'info'` and `'error'` + + +### `read_file(resource, type)` +### `delete_file(resource, type)` +### `write_file(resource, type, data, ...)` +### `list_files(resource, type)` + +With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its + content, deletes the resource, or lists other files under this resource. + +Resource is identified by a path to the file. +A path can contain letters, numbers, characters `-`, `+`, or `_`, and a folder separator: `'/'`. Any other characters are stripped +from the name. Empty descriptors are invalid, except for `list_files` where it means the root folder. + Do not add file extensions to the descriptor - extensions are inferred +based on the `type` of the file. A path can have one `'.zip'` component indicating a zip folder allowing to read / write to and from +zip files, although you cannot nest zip files in other zip files. + +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 no file is present (for read, list and delete operations). Returns `true` +for success writes and deletes, and requested data, based on the file type, for read operations. It returns list of files +for folder listing. + +Supported values for resource `type` are: + * `nbt` - NBT tag + * `json` - JSON file + * `text` - text resource with automatic newlines added + * `raw` - text resource without implied newlines + * `folder` - for `list_files` only - indicating folder listing instead of files + * `shared_nbt`, `shared_text`, `shared_raw`, `shared_folder`, `shared_json` - shared versions of the above + +NBT files have extension `.nbt`, store one NBT tag, and return a NBT type value. JSON files have `.json` extension, store +Scarpet numbers, strings, lists, maps and `null` values. Anything else will be saved as a string (including NBT). +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. 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. + +Throws: +- `nbt_read_error`: When failed to read NBT file. +- `json_read_error`: When failed to read JSON file. The exception data will contain details about the problem. +- `io_exception`: For all other errors when handling data on disk not related to encoding issues + +All other errors resulting of improper use of input arguments should result in `null` returned from the function, rather than exception +thrown. + +
+write_file('foo', 'shared_text, ['one', 'two']);
+write_file('foo', 'shared_text', 'three\n', 'four\n');
+write_file('foo', 'shared_raw', 'five\n', 'six\n');
+
+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 a triple of 0 (unused after success count removal), +intercepted list of output messages, and error message if the command resulted in a failure. +Successful commands return `null` as their error. + +
+run('fill 1 1 1 10 10 10 air') -> [0, ["Successfully filled 123 blocks"], null]
+run('give @s stone 4') -> [0, ["Gave 4 [Stone] to gnembon"], null]
+run('seed') -> [0, ["Seed: [4031384495743822299]"], null]
+run('sed') -> [0, [], "sed<--[HERE]"] // wrong command
+
+ +### `save()` + +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()` + +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 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. + +Throws `nbt_read_error` if failed to read app data. + +### `store_app_data(tag)` + +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`. + +### `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/nbt/mcfunction 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 `enable_hidden_dimensions()` after adding a datapack if needed. + +Synopsis: +
+script run create_datapack('foo', 
+{
+    'foo' -> { 'bar.json' -> {
+        'c' -> true,
+        'd' -> false,
+        'e' -> {'foo' -> [1,2,3]},
+        'a' -> 'foobar',
+        'b' -> 5
+    } }
+})
+
+ +Custom dimension example: +
+// 1.17
+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'
+    } } } } }
+});
+
+// 1.18
+script run a() -> create_datapack('funky_world',  {
+   'data' -> { 'minecraft' -> { 'dimension' -> { 'custom_ow.json' -> { 
+      'type' -> 'minecraft:overworld',
+         'generator' -> {
+            'biome_source' -> {
+               'biomes' -> [
+                  {
+                     'parameters' -> {                        
+                        'erosion' -> [-1.0,1.0], 
+                        'depth' -> 0.0, 
+                        'weirdness' -> [-1.0,1.0],
+                        'offset' -> 0.0,
+                        'temperature' -> [-1.0,1.0],
+                        'humidity' -> [-1.0,1.0],
+                        'continentalness' -> [ -1.2,-1.05]
+                     },
+                     'biome' -> 'minecraft:mushroom_fields'
+                  }
+               ],
+               'type' -> 'minecraft:multi_noise'
+            },
+            'seed' -> 0,
+            'settings' -> 'minecraft:overworld',
+            'type' -> 'minecraft:noise'
+         }
+     } } } }
+});
+enable_hidden_dimensions();  => ['funky_world']
+
+ +Loot table example: +
+script run create_datapack('silverfishes_drop_gravel', {
+    'data' -> { 'minecraft' -> { 'loot_tables' -> { 'entities' -> { 'silverfish.json' -> {
+        'type' -> 'minecraft:entity',
+        'pools' -> [
+            {
+                'rolls' -> {
+                    'min' -> 0,
+                    'max' -> 1
+                },
+                'entries' -> [
+                    {
+                        'type' -> 'minecraft:item',
+                        'name' -> 'minecraft:gravel'
+                    }
+                ]
+            }
+        ]
+    } } } } }
+});
+
+ +Recipe example: +
+script run create_datapack('craftable_cobwebs', {
+    'data' -> { 'scarpet' -> { 'recipes' -> { 'cobweb.json' -> {
+        'type' -> 'crafting_shaped',
+        'pattern' -> [
+            'SSS',
+            'SSS',
+            'SSS'
+        ],
+        'key' -> {
+            'S' -> {
+                'item' -> 'minecraft:string'
+            }
+        },
+        'result' -> {
+            'item' -> 'minecraft:cobweb',
+            'count' -> 1
+        }
+    } } } }
+});
+
+ +Function example: +
+ script run create_datapack('example',{'data/test/functions/talk.mcfunction'->'say 1\nsay 2'})
+
+### `enable_hidden_dimensions()` (1.18.1 and lower) + +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. For example: is not supposed to work at all in vanilla, and its doing exactly that in 1.18.2+. +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 ` (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. + +### `world_time()` + +_**Deprecated**. Use `system_info('world_time')` instead._ + +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()` + +_**Deprecated**. Use `system_info('server_last_tick_times')` instead._ + +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 through 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?)` + +Causes game to run for one tick. By default, it runs it and returns control to the program, but can optionally +accept expected tick length, in milliseconds, waits that extra remaining time and then returns the control to the program. +You can't use it to permanently change the game speed, but setting +longer commands with custom tick speeds can be interrupted via `/script stop` command - if you can get access to the +command terminal. + +Running `game_tick()` as part of the code that runs within the game tick itself is generally a bad idea, +unless you know what this entails. Triggering the `game_tick()` will cause the current (shoulder) tick to pause, then run the internal tick, +then run the rest of the shoulder tick, which may lead to artifacts in between regular code execution and your game simulation code. +If you need to break +up your execution into chunks, you could queue the rest of the work into the next task using `schedule`, or perform your actions +defining `__on_tick()` event handler, but in case you need to take a full control over the game loop and run some simulations using +`game_tick()` as the way to advance the game progress, that might be the simplest way to do it, +and triggering the script in a 'proper' way (there is not 'proper' way, but via command line, or server chat is the most 'proper'), +would be the safest way to do it. For instance, running `game_tick()` from a command block triggered with a button, or in an entity + event triggered in an entity tick, may technically +cause the game to run and encounter that call again, causing stack to overflow. Thankfully it doesn't happen in vanilla running +carpet, but may happen with other modified (modded) versions of the game. + +
+loop(1000,game_tick())  // runs the game as fast as it can for 1000 ticks
+loop(1000,game_tick(100)) // runs the game twice as slow for 1000 ticks
+
+ + +### `seed()` deprecated + +Returns current world seed. Function is deprecated, use `system_info('world_seed')` insteads. + +### `current_dimension()` + +Returns current dimension that the script runs in. + +### `in_dimension(smth, expr)` + +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. + +Throws `unknown_dimension` if provided dimension can't be found. + +### `view_distance()` + +_**Deprecated**. Use `system_info('game_view_distance')` instead._ + +Returns the view distance of the server. + +### `get_mob_counts()`, `get_mob_counts(category)` 1.16+ + +Returns either a map of mob categories with its respective counts and capacities (a.k.a. mobcaps) or just a tuple +of count and limit for a specific category. If a category was not spawning for whatever reason it may not be +returned from `get_mob_counts()`, but could be retrieved for `get_mob_counts(category)`. Returned counts is what spawning +algorithm has taken in to account last time mobs spawned. + +### `schedule(delay, function, args...)` + +Schedules a user defined function to run with a specified `delay` ticks of delay. Scheduled functions run at the end +of the tick, and they will run in order they were scheduled. + +In case you want to schedule a function that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + +### `statistic(player, category, entry)` + +Queries in-game statistics for certain values. Categories include: + +* `mined`: blocks mined +* `crafted`: items crafted +* `used`: items used +* `broken`: items broken +* `picked_up`: items picked up +* `dropped`: items dropped +* `killed`: mobs killed +* `killed_by`: mobs killed by +* `custom`: various random stats + +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 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 effectively has a value of `0`. + + +### `system_info()`, `system_info(property)` +Fetches the value of one of the following system properties. If called without arguments, it returns a list of +available system_info options. It can be used to +fetch various information, mostly not changing, or only available via low level +system calls. In all circumstances, these are only provided as read-only. + +##### Available options in the scarpet app space: + * `app_name` - current app name or `null` if its a default app + * `app_list` - list of all loaded apps excluding default commandline app + * `app_scope` - scope of the global variables and function. Available options is `player` and `global` + * `app_players` - returns a player list that have app run under them. For `global` apps, the list is always empty + +##### 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. + * `world_gamerules` - returns all gamerules in a map form (`rule`->`value`). Like carpet rules, values are returned as strings, so you can use appropriate value conversions using `bool()` or `number()` to convert them to other values. Gamerules are read-only to discourage app programmers to mess up with the settings intentionally applied by server admins. Isn't that just super annoying when a datapack messes up with your gamerule settings? It is still possible to change them though using `run('gamerule ...`. + * `world_spawn_point` - world spawn point in the overworld dimension + * `world_time` - Returns dimension-specific tick counter. + * `world_top` - Returns current dimensions' topmost Y value where one can place blocks. + * `world_bottom` - Returns current dimensions' bottommost Y value where one can place blocks. + * `world_center` - Returns coordinates of the center of the world with respect of the world border + * `world_size` - Returns radius of world border for current dimension. + * `world_max_size` - Returns maximum possible radius of world border for current dimension. + * `world_min_spawning_light` - Returns minimum light level at which mobs can spawn for current dimension, taking into account datapacks + +##### Relevant gameplay related properties + * `game_difficulty` - current difficulty of the game: `'peaceful'`, `'easy'`, `'normal'`, or `'hard'` + * `game_hardcore` - boolean whether the game is in hardcore mode + * `game_storage_format` - format of the world save files, either `'McRegion'` or `'Anvil'` + * `game_default_gamemode` - default gamemode for new players + * `game_max_players` - max allowed players when joining the world + * `game_view_distance` - the view distance + * `game_mod_name` - the name of the base mod. Expect `'fabric'` + * `game_version` - base version of the game + * `game_target` - target release version + * `game_major_target` - major release target. For 1.12.2, that would be 12 + * `game_minor_release` - minor release target. For 1.12.2, that would be 2 + * `game_protocol` - protocol version number + * `game_pack_version` - datapack version number + * `game_data_version` - data version of the game. Returns an integer, so it can be compared. + * `game_stable` - indicating if its a production release or a snapshot + +##### Server related properties + * `server_motd` - the motd of the server visible when joining + * `server_ip` - IP adress of the game hosted + * `server_whitelisted` - boolean indicating whether the access to the server is only for whitelisted players + * `server_whitelist` - list of players allowed to log in + * `server_banned_players` - list of banned player names + * `server_banned_ips` - list of banned IP addresses + * `server_dev_environment` - boolean indicating whether this server is in a development environment. + * `server_mods` - map with all loaded mods mapped to their versions as strings + * `server_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, `system_info('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. + +##### Source related properties + + The source is what is the cause of the code running, with Carpet using it same way as Minecraft commands use to run. Those are used in + some API functions that interact with the game or with commands, and can be manipulated if the execution is caused by an `execute` command, modified + by some functions or ran in non-standard ways. This section provides useful information from these cases (like running from a command + block, right clicking a sign, etc) + * `source_entity` - The entity associated with the execution. This is usually a player (in which case `player()` would get the entity from this), + but it may also be a different entity or `null` if the execution comes from the server console or a command block. + * `source_position` - The position associated with the execution. This is usually the position of the entity, but it may have been manipulated or + it could come from a command block (no entity then). If this call comes from the server console, it will be the world spawn. + * `source_dimension` - The dimension associated with the execution. Execution from the server console provides `overworld` as the dimension. + This can be manipulated by running code inside `in_dimension()`. + * `source_rotation` - The rotation associated with the execution. Usually `[0, 0]` in non-standard situations, the rotation of the entity otherwise. + +##### System related properties + * `java_max_memory` - maximum allowed memory accessible by JVM + * `java_allocated_memory` - currently allocated memory by JVM + * `java_used_memory` - currently used memory by JVM + * `java_cpu_count` - number of processors + * `java_version` - version of Java + * `java_bits` - number indicating how many bits the Java has, 32 or 64 + * `java_system_cpu_load` - current percentage of CPU used by the system + * `java_process_cpu_load` - current percentage of CPU used by JVM + +##### Scarpet related properties + * `scarpet_version` - returns the version of the carpet your scarpet comes with. + +## NBT Storage + +### `nbt_storage()`, `nbt_storage(key)`, `nbt_storage(key, nbt)` +Displays or modifies individual storage NBT tags. With no arguments, returns the list of current NBT storages. With specified `key`, returns the `nbt` associated with current `key`, or `null` if storage does not exist. With specified `key` and `nbt`, sets a new `nbt` value, returning previous value associated with the `key`. +NOTE: This NBT storage is shared with all vanilla datapacks and scripts of the entire server and is persistent between restarts and reloads. You can also access this NBT storage with vanilla `/data storage ...` command. +# `/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 +the executing entity of the command. You will receive tab completion suggestions as you type your code suggesting +functions and global variables. It is advisable to use `/execute in ... at ... as ... run script run ...` or similar, +to simulate running commands in a different scope. + +# `/script load / unload (global?)`, `/script in ` commands + +`load / unload` commands allow for very convenient way of writing your code, providing it to the game and +distribute with your worlds without the need of use of commandblocks. Just place your Scarpet code in the +`/scripts` folder of your world files and make sure it ends with `.sc` extension. In singleplayer, you can +also save your scripts in `.minecraft/config/carpet/scripts` to make them available in any world. + +The good thing about editing that code is that you can not only use normal editing without the need of marking of newlines, +but you can also use comments in your code. + +A comment is anything that starts with a double slash, and continues to the end of the line: + +
+foo = 1;
+//This is a comment
+bar = 2;
+// This never worked, so I commented it out
+// baz = foo()
+
+ +### `/script load/unload (?global)` + +Loading operation will load that script code from disk and execute it right away. You would probably use it to load +some stored procedures to be used for later. To reload the module, just type `/script load` again. Reloading removes +all the current global state (globals and functions) that were added later by the module. To reload all apps along with +all game resources, use vanilla `/reload` command. + + + +Loaded apps have the ability to store and load external files, especially their persistent tag state. For that +check `load_app_data` and `store_app_data` functions. + + + +Unloading the app will only mask their command tree, not remove it. This has the same effect than not having that command +at all, with the exception that if you load a different app with the same name, this may cause commands to reappear. +To remove the commands fully, use `/reload`. + + + +### `/script in ...` + +Allows to run normal /script commands in a specific app, like `run, invoke,..., globals` etc... + +# `/script invoke / invokepoint / invokearea`, `/script globals` commands + +`invoke` family of commands provide convenient way to invoke stored procedures (i.e. functions that has been +defined previously by any running script. To view current stored procedure set, +run `/script globals`(or `/script globals all` to display all functions even hidden ones), to define a new stored +procedure, just run a `/script run function(a,b) -> ( ... )` command with your procedure once, and to forget a +procedure, use `undef` function: `/script run undef('function')` + +### `/script invoke ...` + +Equivalent of running `/script run fun(args, ...)`, but you get the benefit of getting the tab completion of the +command name, and lower permission level required to run these (since player is not capable of running any custom +code in this case, only this that has been executed before by an operator). Arguments will be checked for validity, +and you can only pass simple values as arguments (strings, numbers, or `null` value). Use quotes to include +whitespaces in argument strings. + +Command will check provided arguments with required arguments (count) and fail if not enough or too much +arguments are provided. Operators defining functions are advised to use descriptive arguments names, as these +will be visible for invokers and form the base of understanding what each argument does. + +`invoke` family of commands will tab complete any stored function that does not start with `'_'`, it will still +allow to run procedures starting with `'_'` but not suggest them, and ban execution of any hidden stored procedures, +so ones that start with `'__'`. In case operator needs to use subroutines for convenience and don't want to expose +them to the `invoke` callers, they can use this mechanic. + +
+/script run example_function(const, phrase, price) -> print(const+' '+phrase+' '+price)
+/script invoke example_function pi costs 5
+
+ +### `/script invokepoint ...` + +It is equivalent to `invoke` except it assumes that the first three arguments are coordinates, and provides +coordinates tab completion, with `looking at...` mechanics for convenience. All other arguments are expected +at the end + +### `/script invokearea ...` + +It is equivalent to `invoke` except it assumes that the first three arguments are one set of coordinates, +followed by the second set of coordinates, providing tab completion, with `looking at...` mechanics for convenience, +followed by any other required arguments + +# `/script scan`, `/script fill` and `/script outline` commands + +These commands can be used to evaluate an expression over an area of blocks. They all need to have specified the +origin of the analyzed area (which is used as referenced (0,0,0), and two corners of an area to analyzed. If you +would want that the script block coordinates refer to the actual world coordinates, use origin of `0 0 0`, or if +it doesn't matter, duplicating coordinates of one of the corners is the easiest way. + +These commands, unlike raw `/script run` are limited by vanilla fill / clone command limit of 32k blocks, which can +be altered with carpet mod's own `/carpet fillLimit` command. + +### `/script scan origin corner corner expr` + +Evaluates expression for each point in the area and returns number of successes (result was positive). Since the +command by itself doesn't affect the area, the effects would be in side effects. + +### `/script fill origin corner corner "expr" (? replace )` + +Think of it as a regular fill command, that sets blocks based on whether a result of the command was successful. +Note that the expression is in quotes. Thankfully string constants in `scarpet` use single quotes. Can be used to +fill complex geometric shapes. + +### `/script outline origin corner corner "expr" (? replace )` + +Similar to `fill` command it evaluates an expression for each block in the area, but in this case setting blocks +where condition was true and any of the neighbouring blocks were evaluated negatively. This allows to create surface +areas, like sphere for example, without resorting to various rounding modes and tricks. + +Here is an example of seven ways to draw a sphere of radius of 32 blocks around 0 100 0: + +
+/script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <  32*32" white_stained_glass replace air
+/script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <= 32*32" white_stained_glass replace air
+/script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <  32.5*32.5" white_stained_glass replace air
+/script fill    0 100 0 -40 60 -40 40 140 40 "floor(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+/script fill    0 100 0 -40 60 -40 40 140 40 "round(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+/script fill    0 100 0 -40 60 -40 40 140 40 "ceil(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+/draw sphere 0 100 0 32 white_stained_glass replace air // fluffy ball round(sqrt(x*x+y*y+z*z)-rand(abs(y)))==32
+
+ +The last method is the one that world edit is using (part of carpet mod). It turns out that the outline method +with `32.5` radius, fill method with `round` function and draw command are equivalent + +# `script stop/script resume` command + +`/script stop` allows to stop execution of any script currently running that calls the `game_tick()` function which +allows the game loop to regain control of the game and process other commands. This will also make sure that all +current and future programs will stop their execution. Execution of all programs will be prevented +until `/script resume` command is called. + +Lets look at the following example. This is a program computes Fibonacci number in a recursive manner: + +
+fib(n) -> if(n<3, 1, fib(n-1)+fib(n-2) ); fib(8)
+
+ +That's really bad way of doing it, because the higher number we need to compute the compute requirements will +rise exponentially with `n`. It takes a little over 50 milliseconds to do fib(24), so above one tick, but about +a minute to do fib(40). Calling fib(40) will not only freeze the game, but also you woudn't be able to interrupt +its execution. We can modify the script as follows + +
+fib(n) -> ( game_tick(50); if(n<3, 1, fib(n-1)+fib(n-2) ) ); fib(40)
+
+ +But this would never finish as such call would finish after `~ 2^40` ticks. To make our computations responsive, +yet able to respond to user interactions, other commands, as well as interrupt execution, we could do the following: + +
+fib(n) -> ( if(n==23, game_tick(50) ); if(n<3, 1, fib(n-1)+fib(n-2) ) ); fib(40)
+
+ +This would slow down the computation of fib(40) from a minute to two, but allows the game to keep continue running +and be responsive to commands, using about half of each tick to advance the computation. Obviously depending on the +problem, and available hardware, certain things can take more or less time to execute, so portioning of work with +calling `gametick` should be balanced in each case separately + +# `/script download` command + +`/script download ` command allows downloading and running apps directly from an online app store (it's all free), +by default the [scarpet app store](https://www.github.com/gnembon/scarpet). +Downloaded apps will be placed in the world's scripts folder automatically. Location of the app store is controlled +with a global carpet setting of `/carpet scriptsAppStore`. Apps, if required, will also download all the resources they need +to run it. Consecutive downloads of the same app will re-download its content and its resources, but will not remove anything +that has been removed or renamed. + +# `/script remove` command + +command allow to stop and remove apps installed in the worlds scripts folder. The app is unloaded and app 'sc' file is moved +to the `/scripts/trash`. Removed apps can only be restored by manually moving it back from the trash folder, +or by redownloading from the appstore. diff --git a/docs/scarpet/api/Auxiliary.md b/docs/scarpet/api/Auxiliary.md new file mode 100644 index 0000000..8427523 --- /dev/null +++ b/docs/scarpet/api/Auxiliary.md @@ -0,0 +1,821 @@ +# Auxiliary aspects + +Collection of other methods that control smaller, yet still important aspects of the game + +## Sounds + +### `sound()`, `sound(name, pos, volume?, pitch?, mixer?)` + +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 three numbers. Uses the same options as a + corresponding `playsound` command. + +Used with no arguments, returns a list of available sound names. Note that this list may not include all sounds that +clients will actually be able to receive (they may have more available via resourcepacks for example). + +## Particles + +### `particle()`, `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'`. + +Used with no arguments, return the list of available particle names. Note that some of the names do not correspond to a valid +particle that can be fed to `particle(...)` function due to a fact that some particles need more configuration +to be valid, like `dust`, `block` etc. Should be used as a reference only. + +Throws `unknown_particle` if particle doesn't exist. + +### `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. If a player (or player name) is supplied, only +that player will receive particles. + +Throws `unknown_particle` if particle doesn't exist. + +### `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. + +Throws `unknown_particle` if particle doesn't exist. + +## Markers + +### `draw_shape(shape, duration, key?, value?, ... )`, +### `draw_shape(shape, duration, [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. The drawback of sending shapes is batches is that they need to address +the same list of players, i.e. if multiple players from the list target different players, all shapes will be sent to all of them. + +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, or a list of players. If specified, the shapes will appear only for the specified + players (regardless where they are), otherwise it will be send to all players in the current dimension. + * `line` - (Deprecated) line thickness, defaults to 2.0pt. Not supported in 1.17's 3.2 core GL renderer. + * `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. Preceeding an axis + with `d`, like `dxdydz` would make so that entity position is treated discretely (rounded down). + * `debug` - if True, it will only be visible when F3+B entity bounding boxes is enabled. + * `facing` - applicable only to `'text'`, `'block'` or '`item'` shapes, where its facing. Possible options are: + * `player`: Default. Element always rotates to face the player eye position, + * `camera`: Element is placed on the plane orthogonal to player look vector, + * `north`, `south`, `east`, `west`, `up`, `down`: obvious + +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'`. + + * `'polygon'`: + * Required attributes: + * `points` - list of points defining vertices of the polygon + * Optional attributes: + * `relative` - list of bools. vertices of the polygon that affected by 'follow'. Could be a single bools to affact allpoints too. Default means that every point is affacted. + * `mode` - how those points are connected. may be "polygon"(default),"strip" or "triangles". "polygon" means that it will be viewed as vertices of a polygon center on the first one. "strip" means that it will be viewed as a triangles strip. "triangles" means that it will be viewed as some triangles that are not related to each other (therefor length of `points` in this mode have to be a multiple of 3). + * `inner` - if `true` it will make the inner edges be drawn as well. + * `doublesided` - if `true` it will make the shapes visible from the back as well. Default is `true`. + + * `'label'` - draws a text in the world. Default `line` attribute controls main font color. + `fill` controls the color of the background. + * Required attributes: + * `pos` - position + * `text` - string or formatted text to display + * Optional attributes + * `value` - string or formatted text to display instead of the main `text`. `value` unlike `text` + is not used to determine uniqueness of the drawn text so can be used to + display smoothly dynamic elements where value of an element is constantly + changing and updates to it are being sent from the server. + * `size` - float. Default font size is 10. + * `doublesided` - if `true` it will make the text visible from the back as well. Default is `false` (1.16+) + * `align` - text alignment with regards to `pos`. Default is `center` (displayed text is + centered with respect to `pos`), `left` (`pos` indicates beginning of text), and `right` (`pos` + indicates the end of text). + * `tilt`, `lean`, `turn` - additional rotations of the text on the canvas along all three axis + * `indent`, `height`, `raise` - offsets for text rendering on X (`indent`), Y (`height`), and Z axis (`raise`) + with regards to the plane of the text. One unit of these corresponds to 1 line spacing, which + can be used to display multiple lines of text bound to the same `pos` + + * `'block'`: draws a block at the specified position: + * Required attributes: + * `pos` - position of the object. + * `block` - the object to show. It is a block value or a name of a block with optional NBT data. + * Optional attributes: + * `tilt`, `lean`, `turn` - additional rotations along all three axis. It uses the block center as the origin. + * `scale` - scale of it in 3 axis-direction. should be a number or a list of 3 numbers (x,y,z). + * `skylight`, `blocklight` - light level. omit it to use local light level. should between 0~15. + + * `'item'`: draws an item at the specified position: + * Required attributes: + * `pos` - position of the object. + * `item` - the object to show. It is an item tuple or a string identified item that may have NBT data. + * Optional attributes: + * `tilt`, `lean`, `turn` - additional rotations along all three axis. for `block`, it use its block center as the origin. + * `scale` - scale of it in 3 axis-direction. should be a number or a list of 3 numbers (x,y,z). + * `skylight`, `blocklight` - light level. omit it to use local light level. should between 0~15. + * `variant` - one of `'none'`, `'thirdperson_lefthand'`, `'thirdperson_righthand'`, `'firstperson_lefthand'`, + `'firstperson_righthand'`, `'head'`, `'gui'`, `'ground'`, `'fixed'`. In addition to the literal meaning, + it can also be used to use special models of tridents and telescopes. + This attribute is experimental and use of it will change in the future. + + +### `create_marker(text, pos, rotation?, block?, interactive?)` + +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. + +If `interactive` (`true` by default) is `false`, the armorstand will be a marker and would not be interactive in any +gamemode. But blocks can be placed inside markers and will not catch any interaction events. + +Y Position of a marker text or block will be adjusted to make blocks or text appear at the specified position. +This makes so that actual armorstand position may be offset on Y axis. You would need to adjust your entity +locations if you plan to move the armorstand around after the fact. If both text and block are specified - one of them +will be aligned (armorstand type markers text shows up at their feet, while for regular armorstands - above the head, +while block on the head always render in the same position regardless if its a marker or not). + + +### `remove_all_markers()` + +Removes all scarpet markers from the loaded portion of the world created by this app, in case you didn't want to do + the proper cleanup. + +## System function + +### `nbt(expr)` + +Treats the argument as a nbt serializable string and returns its nbt value. In case nbt is not in a correct nbt +compound tag format, it will return `null` value. + +Consult section about container operations in `Expression` to learn about possible operations on nbt values. + +### `escape_nbt(expr)` + +Excapes all the special characters in the string or nbt tag and returns a string that can be stored in nbt directly +as a string value. + +### `tag_matches(daddy_tag, baby_tag, match_lists?)` + +Utility returning `true` if `baby_tag` is fully contained in `daddy_tag`. Anything matches `null` baby tag, and +Nothing is contained in a `null` daddy tag. If `match_lists` is specified and `false`, content of nested lists is ignored. +Default behaviour is to match them. + +### `parse_nbt(tag)` + +Converts NBT tag to a scarpet value, which you can navigate through much better. + +Converts: + - Compound tags into maps with string keys + - List tags into list values + - Numbers (Ints, Floats, Doubles, Longs) into a number + - Rest is converted to strings. + +### `encode_nbt(expr, force?)` + +Encodes value of the expression as an NBT tag. By default (or when `force` is false), it will only allow +to encode values that are guaranteed to return the same value when applied the resulting tag to `parse_nbt()`. +Supported types that can reliably convert back and forth to and from NBT values are: + - Maps with string keywords + - Lists of items of the same type (scarpet will take care of unifying value types if possible) + - Numbers (encoded as Ints -> Longs -> Doubles, as needed) + - Strings + +Other value types will only be converted to tags (including NBT tags) if `force` is true. They would require +extra treatment when loading them back from NBT, but using `force` true will always produce output / never +produce an exception. + +### `print(expr)`, `print(player/player_list, expr)` + +Displays the result of the expression to the chat. Overrides default `scarpet` behaviour of sending everything to stderr. +For player scoped apps it always by default targets the player for whom the app runs on behalf. +Can optionally define player or list of players to send the message to. + +### `format(components, ...)`, `format([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. + * `'@'` - a URL that will be opened when the text below it is clicked. + * `'&'` - a text that will be copied to clipboard 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
+
+ +### `item_display_name(item)` + Returns the name of the item as a Text Value. `item` should be a list of `[item_name, count, nbt]`, or just an item name. + + Please note that it is a translated value. treating it like a string (eg.slicing, breaking, changing its case) will turn it back into a normal string without translatable properties. just like a colorful formatted text loose its color. And the result of it converting to a string will use en-us (in a server) or your single player's language, but when you use print() or others functions that accept a text value to broadcast it to players, it will use each player's own language. + + If the item is renamed, it will also be reflected in the results. + + +### `display_title(players, type, text?, fadeInTicks?, stayTicks?, fadeOutTicks),` + +Sends the player (or players if `players` is a list) a title of a specific type, with optionally some times. + * `players` is either an online player or a list of players. When sending a single player, it will throw if the player is invalid or offline. + * `type` is either `'title'`, `'subtitle'`, `actionbar` or `clear`. + Note: `subtitle` will only be displayed if there is a title being displayed (can be an empty one) + * `title` is what title to send to the player. It is required except for `clear` type. Can be a text formatted using `format()` + * `...Ticks` are the number of ticks the title will stay in that state. + If not specified, it will use current defaults (those defaults may have changed from a previous `/title times` execution). + Executing with those will set the times to the specified ones. + Note that `actionbar` type doesn't support changing times (vanilla bug, see [MC-106167](https://bugs.mojang.com/browse/MC-106167)). + +### `display_title(players, 'player_list_header', text)` +### `display_title(players, 'player_list_footer', text)` + +Changes the header or footer of the player list for the specified targets. +If `text` is `null` or an empty string it will remove the header or footer for the specified targets. +In case the player has Carpet loggers running, the footer specified by Scarpet will appear above the loggers. + +### `logger(msg), logger(type, msg)` + +Prints the message to system logs, and not to chat. +By default prints an info, unless you specify otherwise in the `type` parameter. + +Available output types: + +`'debug'`, `'warn'`, `'fatal'`, `'info'` and `'error'` + + +### `read_file(resource, type)` +### `delete_file(resource, type)` +### `write_file(resource, type, data, ...)` +### `list_files(resource, type)` + +With the specified `resource` in the scripts folder, of a specific `type`, writes/appends `data` to it, reads its + content, deletes the resource, or lists other files under this resource. + +Resource is identified by a path to the file. +A path can contain letters, numbers, characters `-`, `+`, or `_`, and a folder separator: `'/'`. Any other characters are stripped +from the name. Empty descriptors are invalid, except for `list_files` where it means the root folder. + Do not add file extensions to the descriptor - extensions are inferred +based on the `type` of the file. A path can have one `'.zip'` component indicating a zip folder allowing to read / write to and from +zip files, although you cannot nest zip files in other zip files. + +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 no file is present (for read, list and delete operations). Returns `true` +for success writes and deletes, and requested data, based on the file type, for read operations. It returns list of files +for folder listing. + +Supported values for resource `type` are: + * `nbt` - NBT tag + * `json` - JSON file + * `text` - text resource with automatic newlines added + * `raw` - text resource without implied newlines + * `folder` - for `list_files` only - indicating folder listing instead of files + * `shared_nbt`, `shared_text`, `shared_raw`, `shared_folder`, `shared_json` - shared versions of the above + +NBT files have extension `.nbt`, store one NBT tag, and return a NBT type value. JSON files have `.json` extension, store +Scarpet numbers, strings, lists, maps and `null` values. Anything else will be saved as a string (including NBT). +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. 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. + +Throws: +- `nbt_read_error`: When failed to read NBT file. +- `json_read_error`: When failed to read JSON file. The exception data will contain details about the problem. +- `io_exception`: For all other errors when handling data on disk not related to encoding issues + +All other errors resulting of improper use of input arguments should result in `null` returned from the function, rather than exception +thrown. + +
+write_file('foo', 'shared_text, ['one', 'two']);
+write_file('foo', 'shared_text', 'three\n', 'four\n');
+write_file('foo', 'shared_raw', 'five\n', 'six\n');
+
+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 a triple of 0 (unused after success count removal), +intercepted list of output messages, and error message if the command resulted in a failure. +Successful commands return `null` as their error. + +
+run('fill 1 1 1 10 10 10 air') -> [0, ["Successfully filled 123 blocks"], null]
+run('give @s stone 4') -> [0, ["Gave 4 [Stone] to gnembon"], null]
+run('seed') -> [0, ["Seed: [4031384495743822299]"], null]
+run('sed') -> [0, [], "sed<--[HERE]"] // wrong command
+
+ +### `save()` + +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()` + +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 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. + +Throws `nbt_read_error` if failed to read app data. + +### `store_app_data(tag)` + +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`. + +### `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/nbt/mcfunction 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 `enable_hidden_dimensions()` after adding a datapack if needed. + +Synopsis: +
+script run create_datapack('foo', 
+{
+    'foo' -> { 'bar.json' -> {
+        'c' -> true,
+        'd' -> false,
+        'e' -> {'foo' -> [1,2,3]},
+        'a' -> 'foobar',
+        'b' -> 5
+    } }
+})
+
+ +Custom dimension example: +
+// 1.17
+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'
+    } } } } }
+});
+
+// 1.18
+script run a() -> create_datapack('funky_world',  {
+   'data' -> { 'minecraft' -> { 'dimension' -> { 'custom_ow.json' -> { 
+      'type' -> 'minecraft:overworld',
+         'generator' -> {
+            'biome_source' -> {
+               'biomes' -> [
+                  {
+                     'parameters' -> {                        
+                        'erosion' -> [-1.0,1.0], 
+                        'depth' -> 0.0, 
+                        'weirdness' -> [-1.0,1.0],
+                        'offset' -> 0.0,
+                        'temperature' -> [-1.0,1.0],
+                        'humidity' -> [-1.0,1.0],
+                        'continentalness' -> [ -1.2,-1.05]
+                     },
+                     'biome' -> 'minecraft:mushroom_fields'
+                  }
+               ],
+               'type' -> 'minecraft:multi_noise'
+            },
+            'seed' -> 0,
+            'settings' -> 'minecraft:overworld',
+            'type' -> 'minecraft:noise'
+         }
+     } } } }
+});
+enable_hidden_dimensions();  => ['funky_world']
+
+ +Loot table example: +
+script run create_datapack('silverfishes_drop_gravel', {
+    'data' -> { 'minecraft' -> { 'loot_tables' -> { 'entities' -> { 'silverfish.json' -> {
+        'type' -> 'minecraft:entity',
+        'pools' -> [
+            {
+                'rolls' -> {
+                    'min' -> 0,
+                    'max' -> 1
+                },
+                'entries' -> [
+                    {
+                        'type' -> 'minecraft:item',
+                        'name' -> 'minecraft:gravel'
+                    }
+                ]
+            }
+        ]
+    } } } } }
+});
+
+ +Recipe example: +
+script run create_datapack('craftable_cobwebs', {
+    'data' -> { 'scarpet' -> { 'recipes' -> { 'cobweb.json' -> {
+        'type' -> 'crafting_shaped',
+        'pattern' -> [
+            'SSS',
+            'SSS',
+            'SSS'
+        ],
+        'key' -> {
+            'S' -> {
+                'item' -> 'minecraft:string'
+            }
+        },
+        'result' -> {
+            'item' -> 'minecraft:cobweb',
+            'count' -> 1
+        }
+    } } } }
+});
+
+ +Function example: +
+ script run create_datapack('example',{'data/test/functions/talk.mcfunction'->'say 1\nsay 2'})
+
+### `enable_hidden_dimensions()` (1.18.1 and lower) + +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. For example: is not supposed to work at all in vanilla, and its doing exactly that in 1.18.2+. +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 ` (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. + +### `world_time()` + +_**Deprecated**. Use `system_info('world_time')` instead._ + +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()` + +_**Deprecated**. Use `system_info('server_last_tick_times')` instead._ + +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 through 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?)` + +Causes game to run for one tick. By default, it runs it and returns control to the program, but can optionally +accept expected tick length, in milliseconds, waits that extra remaining time and then returns the control to the program. +You can't use it to permanently change the game speed, but setting +longer commands with custom tick speeds can be interrupted via `/script stop` command - if you can get access to the +command terminal. + +Running `game_tick()` as part of the code that runs within the game tick itself is generally a bad idea, +unless you know what this entails. Triggering the `game_tick()` will cause the current (shoulder) tick to pause, then run the internal tick, +then run the rest of the shoulder tick, which may lead to artifacts in between regular code execution and your game simulation code. +If you need to break +up your execution into chunks, you could queue the rest of the work into the next task using `schedule`, or perform your actions +defining `__on_tick()` event handler, but in case you need to take a full control over the game loop and run some simulations using +`game_tick()` as the way to advance the game progress, that might be the simplest way to do it, +and triggering the script in a 'proper' way (there is not 'proper' way, but via command line, or server chat is the most 'proper'), +would be the safest way to do it. For instance, running `game_tick()` from a command block triggered with a button, or in an entity + event triggered in an entity tick, may technically +cause the game to run and encounter that call again, causing stack to overflow. Thankfully it doesn't happen in vanilla running +carpet, but may happen with other modified (modded) versions of the game. + +
+loop(1000,game_tick())  // runs the game as fast as it can for 1000 ticks
+loop(1000,game_tick(100)) // runs the game twice as slow for 1000 ticks
+
+ + +### `seed()` deprecated + +Returns current world seed. Function is deprecated, use `system_info('world_seed')` insteads. + +### `current_dimension()` + +Returns current dimension that the script runs in. + +### `in_dimension(smth, expr)` + +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. + +Throws `unknown_dimension` if provided dimension can't be found. + +### `view_distance()` + +_**Deprecated**. Use `system_info('game_view_distance')` instead._ + +Returns the view distance of the server. + +### `get_mob_counts()`, `get_mob_counts(category)` 1.16+ + +Returns either a map of mob categories with its respective counts and capacities (a.k.a. mobcaps) or just a tuple +of count and limit for a specific category. If a category was not spawning for whatever reason it may not be +returned from `get_mob_counts()`, but could be retrieved for `get_mob_counts(category)`. Returned counts is what spawning +algorithm has taken in to account last time mobs spawned. + +### `schedule(delay, function, args...)` + +Schedules a user defined function to run with a specified `delay` ticks of delay. Scheduled functions run at the end +of the tick, and they will run in order they were scheduled. + +In case you want to schedule a function that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + +### `statistic(player, category, entry)` + +Queries in-game statistics for certain values. Categories include: + +* `mined`: blocks mined +* `crafted`: items crafted +* `used`: items used +* `broken`: items broken +* `picked_up`: items picked up +* `dropped`: items dropped +* `killed`: mobs killed +* `killed_by`: mobs killed by +* `custom`: various random stats + +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 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 effectively has a value of `0`. + + +### `system_info()`, `system_info(property)` +Fetches the value of one of the following system properties. If called without arguments, it returns a list of +available system_info options. It can be used to +fetch various information, mostly not changing, or only available via low level +system calls. In all circumstances, these are only provided as read-only. + +##### Available options in the scarpet app space: + * `app_name` - current app name or `null` if its a default app + * `app_list` - list of all loaded apps excluding default commandline app + * `app_scope` - scope of the global variables and function. Available options is `player` and `global` + * `app_players` - returns a player list that have app run under them. For `global` apps, the list is always empty + +##### 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. + * `world_gamerules` - returns all gamerules in a map form (`rule`->`value`). Like carpet rules, values are returned as strings, so you can use appropriate value conversions using `bool()` or `number()` to convert them to other values. Gamerules are read-only to discourage app programmers to mess up with the settings intentionally applied by server admins. Isn't that just super annoying when a datapack messes up with your gamerule settings? It is still possible to change them though using `run('gamerule ...`. + * `world_spawn_point` - world spawn point in the overworld dimension + * `world_time` - Returns dimension-specific tick counter. + * `world_top` - Returns current dimensions' topmost Y value where one can place blocks. + * `world_bottom` - Returns current dimensions' bottommost Y value where one can place blocks. + * `world_center` - Returns coordinates of the center of the world with respect of the world border + * `world_size` - Returns radius of world border for current dimension. + * `world_max_size` - Returns maximum possible radius of world border for current dimension. + * `world_min_spawning_light` - Returns minimum light level at which mobs can spawn for current dimension, taking into account datapacks + +##### Relevant gameplay related properties + * `game_difficulty` - current difficulty of the game: `'peaceful'`, `'easy'`, `'normal'`, or `'hard'` + * `game_hardcore` - boolean whether the game is in hardcore mode + * `game_storage_format` - format of the world save files, either `'McRegion'` or `'Anvil'` + * `game_default_gamemode` - default gamemode for new players + * `game_max_players` - max allowed players when joining the world + * `game_view_distance` - the view distance + * `game_mod_name` - the name of the base mod. Expect `'fabric'` + * `game_version` - base version of the game + * `game_target` - target release version + * `game_major_target` - major release target. For 1.12.2, that would be 12 + * `game_minor_release` - minor release target. For 1.12.2, that would be 2 + * `game_protocol` - protocol version number + * `game_pack_version` - datapack version number + * `game_data_version` - data version of the game. Returns an integer, so it can be compared. + * `game_stable` - indicating if its a production release or a snapshot + +##### Server related properties + * `server_motd` - the motd of the server visible when joining + * `server_ip` - IP adress of the game hosted + * `server_whitelisted` - boolean indicating whether the access to the server is only for whitelisted players + * `server_whitelist` - list of players allowed to log in + * `server_banned_players` - list of banned player names + * `server_banned_ips` - list of banned IP addresses + * `server_dev_environment` - boolean indicating whether this server is in a development environment. + * `server_mods` - map with all loaded mods mapped to their versions as strings + * `server_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, `system_info('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. + +##### Source related properties + + The source is what is the cause of the code running, with Carpet using it same way as Minecraft commands use to run. Those are used in + some API functions that interact with the game or with commands, and can be manipulated if the execution is caused by an `execute` command, modified + by some functions or ran in non-standard ways. This section provides useful information from these cases (like running from a command + block, right clicking a sign, etc) + * `source_entity` - The entity associated with the execution. This is usually a player (in which case `player()` would get the entity from this), + but it may also be a different entity or `null` if the execution comes from the server console or a command block. + * `source_position` - The position associated with the execution. This is usually the position of the entity, but it may have been manipulated or + it could come from a command block (no entity then). If this call comes from the server console, it will be the world spawn. + * `source_dimension` - The dimension associated with the execution. Execution from the server console provides `overworld` as the dimension. + This can be manipulated by running code inside `in_dimension()`. + * `source_rotation` - The rotation associated with the execution. Usually `[0, 0]` in non-standard situations, the rotation of the entity otherwise. + +##### System related properties + * `java_max_memory` - maximum allowed memory accessible by JVM + * `java_allocated_memory` - currently allocated memory by JVM + * `java_used_memory` - currently used memory by JVM + * `java_cpu_count` - number of processors + * `java_version` - version of Java + * `java_bits` - number indicating how many bits the Java has, 32 or 64 + * `java_system_cpu_load` - current percentage of CPU used by the system + * `java_process_cpu_load` - current percentage of CPU used by JVM + +##### Scarpet related properties + * `scarpet_version` - returns the version of the carpet your scarpet comes with. + +## NBT Storage + +### `nbt_storage()`, `nbt_storage(key)`, `nbt_storage(key, nbt)` +Displays or modifies individual storage NBT tags. With no arguments, returns the list of current NBT storages. With specified `key`, returns the `nbt` associated with current `key`, or `null` if storage does not exist. With specified `key` and `nbt`, sets a new `nbt` value, returning previous value associated with the `key`. +NOTE: This NBT storage is shared with all vanilla datapacks and scripts of the entire server and is persistent between restarts and reloads. You can also access this NBT storage with vanilla `/data storage ...` command. diff --git a/docs/scarpet/api/BlockIterations.md b/docs/scarpet/api/BlockIterations.md new file mode 100644 index 0000000..fccce48 --- /dev/null +++ b/docs/scarpet/api/BlockIterations.md @@ -0,0 +1,63 @@ +# Iterating over larger areas of blocks + +These functions help scan larger areas of blocks without using generic loop functions, like nested `loop`. + +### `scan(center, range, upper_range?, expr)` + +Evaluates expression over area of blocks defined by its center `center = (cx, cy, cz)`, expanded in all directions +by `range = (dx, dy, dz)` blocks, or optionally in negative with `range` coords, and `upper_range` coords in +positive values. That means that if you want a box starting at the northwest coord with given base, width and height +dimensions, you can do `'scan(center, 0, 0, 0, w, h, d, ...)`. + +`center` can be defined either as three coordinates, a single tuple of three coords, or a block value. +`range` and `upper_range` can have the same representations, just if they are block values, it computes the distance to +the center as range instead of taking the values as is. That way you can iterate from the center to a box whose surface +area constains the `range` and/or `upper_range` blocks. + +`expr` receives `_x, _y, _z` variables 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. + +`scan` also handles `continue` and `break` statements, using `continue`'s return value to use in place of expression +return value. `break` return value has no effect. + +### `volume(from_pos, to_pos, expr)` + +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. +You can use a position or three coordinates to specify, it doesn't matter. + +For return value and handling `break` and `continue` statements, see `scan` function above. + +### `neighbours(pos)` + +Returns the list of 6 neighbouring blocks to the argument. Commonly used with other loop functions like `for`. + +
+for(neighbours(x,y,z),air(_)) => 4 // number of air blocks around a block
+
+ +### `rect(center, range?, upper_range?)` + +Returns an iterator, just like `range` function that iterates over a rectangular area of blocks. If only center +point is specified, it iterates over 27 blocks (range of 1). If `range` arguments are specified, expands selection by +the respective number of blocks in each direction. If `upper_range` arguments are specified, it uses `range` for +negative offset, and `upper_range` for positive, similar to `scan`. + +Basically the arguments are the same as the first three arguments of `scan`, except this function returns the list of +blocks that `scan` would evaluate over. If you are going to iterate over these blocks, like `for(rect(args), do_something())`, +then `scan(args, do_something())` is an equivalent, yet more compute-friendly alternative, especially for very large areas. + +`center` can be defined either as three coordinates, a list of three coords, or a block value. +`range` and `upper_range` can have the same representations, just if they are block values, it computes the distance to the center +as range instead of taking the values as is.` + +### `diamond(center_pos, radius?, height?)` + +Iterates over a diamond like area of blocks. With no radius and height, its 7 blocks centered around the middle +(block + neighbours). With a radius specified, it expands shape on x and z coords, and with a custom height, on y. +Any of these can be zero as well. radius of 0 makes a stick, height of 0 makes a diamond shape pad. + +If radius and height are the same, creats a 3D diamond, of all the blocks which are a manhattan distance of `radius` away +from the center. diff --git a/docs/scarpet/api/BlocksAndWorldAccess.md b/docs/scarpet/api/BlocksAndWorldAccess.md new file mode 100644 index 0000000..d3f3c10 --- /dev/null +++ b/docs/scarpet/api/BlocksAndWorldAccess.md @@ -0,0 +1,837 @@ +# Blocks / World API + +## Specifying blocks + +### `block(x, y, z)`, `block([x,y,z])`, `block(state)` + +Returns either a block from specified location, or block with a specific state (as used by `/setblock` command), +so allowing for block properties, block entity data etc. Blocks otherwise can be referenced everywhere by its simple +string name, but its only used in its default state. + +
+block('air')  => air
+block('iron_trapdoor[half=top]')  => iron_trapdoor
+block(0,0,0) == block('bedrock')  => 1
+block('hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') => hopper
+
+ +Retrieving a block with `block` function has also a side-effect of evaluating its current state and data. +So if you use it later it will reflect block state and data of the block that was when block was called, rather than +when it was used. Block values passed in various places like `scan` functions, etc, are not fully evaluated unless +its properties are needed. This means that if the block at the location changes before its queried in the program this +might result in getting the later state, which might not be desired. Consider the following example: + +Throws `unknown_block` if provided input is not valid. + +
set(10,10,10,'stone');
+scan(10,10,10,0,0,0, b = _);
+set(10,10,10,'air');
+print(b); // 'air', block was remembered 'lazily', and evaluated by `print`, when it was already set to air
+set(10,10,10,'stone');
+scan(10,10,10,0,0,0, b = block(_));
+set(10,10,10,'air');
+print(b); // 'stone', block was evaluated 'eagerly' but call to `block`
+
+ +## World Manipulation + +All the functions below can be used with block value, queried with coord triple, or 3-long list. All `pos` in the +functions referenced below refer to either method of passing block position. + +### `set(pos, block, property?, value?, ..., block_data?)`, `set(pos, block, [property?, value?, ...], block_data?)`, `set(pos, block, {property? -> value?, ...}, block_data?)` + +First argument for the `set` function is either a coord triple, list of three numbers, or a world localized block value. +Second argument, `block`, is either an existing block value, a result of `block()` function, or a string value indicating the block name +with optional state and block data. It is then followed by an optional +`property - value` pairs for extra block state (which can also be provided in a list or a map). Optional `block_data` include the block data to +be set for the target block. + +If `block` is specified only by name, then if a +destination block is the same the `set` operation is skipped, otherwise is executed, for other potential extra +properties that the original source block may have contained. + +The returned value is either the block state that has been set, or `false` if block setting was skipped, or failed + +Throws `unknown_block` if provided block to set is not valid + +
+set(0,5,0,'bedrock')  => bedrock
+set([0,5,0], 'bedrock')  => bedrock
+set(block(0,5,0), 'bedrock')  => bedrock
+scan(0,5,0,0,0,0,set(_,'bedrock'))  => 1
+set(pos(player()), 'bedrock')  => bedrock
+set(0,0,0,'bedrock')  => 0   // or 1 in overworlds generated in 1.8 and before
+scan(0,100,0,20,20,20,set(_,'glass'))
+    // filling the area with glass
+scan(0,100,0,20,20,20,set(_,block('glass')))
+    // little bit faster due to internal caching of block state selectors
+b = block('glass'); scan(0,100,0,20,20,20,set(_,b))
+    // yet another option, skips all parsing
+set(x,y,z,'iron_trapdoor')  // sets bottom iron trapdoor
+
+set(x,y,z,'iron_trapdoor[half=top]')  // sets the top trapdoor
+set(x,y,z,'iron_trapdoor','half','top') // also correct - top trapdoor
+set(x,y,z,'iron_trapdoor', ['half','top']) // same
+set(x,y,z,'iron_trapdoor', {'half' -> 'top'}) // same
+set(x,y,z, block('iron_trapdoor[half=top]')) // also correct, block() provides extra parsing of block state
+
+set(x,y,z,'hopper[facing=north]{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') // extra block data
+set(x,y,z,'hopper', {'facing' -> 'north'}, nbt('{Items:[{Slot:1b,id:"minecraft:slime_ball",Count:16b}]}') ) // same
+
+ +### `without_updates(expr)` + +Evaluates subexpression without causing updates when blocks change in the world. + +For synchronization sake, as well as from the fact that suppressed update can only happen within a tick, +the call to the `expr` is docked on the main server task. + +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: + +
+__config() -> {'scope' -> 'global'};
+__on_chunk_generated(x, z) -> (
+  scan(x,0,z,0,0,0,15,15,15,
+    if (perlin(_x/16, _y/8, _z/16) > _y/16,
+      set(_, 'black_stained_glass');
+    )
+  )
+)
+
+ +The following addition resolves this issue, by not allowing block updates past chunk borders: + +
+__config() -> {'scope' -> 'global'};
+__on_chunk_generated(x, z) -> (
+  without_updates(
+    scan(x,0,z,0,0,0,15,15,15,
+      if (perlin(_x/16, _y/8, _z/16) > _y/16,
+        set(_, 'black_stained_glass');
+      )
+    )
+  )
+)
+
+ +### `place_item(item, pos, facing?, sneak?)` + +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. + +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. + +Throws `unknown_item` if `item` doesn't exist + +
+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?)` + +Sets a Point of Interest (POI) of a specified type with optional custom occupancy. By default new POIs are not occupied. +If type is `null`, POI at position is removed. In any case, previous POI is also removed. Available POI types are: + +* `'unemployed', 'armorer', 'butcher', 'cartographer', 'cleric', 'farmer', 'fisherman', 'fletcher', 'leatherworker', 'librarian', 'mason', 'nitwit', 'shepherd', 'toolsmith', 'weaponsmith', 'home', 'meeting', 'beehive', 'bee_nest', 'nether_portal'` + +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. + +Throws `unknown_poi` if the provided point of interest doesn't exist + +### `set_biome(pos, biome_name, update=true)` + +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 sent to clients with the entire data from the chunk. + +Be aware that depending on the MC version and dimension settings biome can be set either in a 1x1x256 +column or 4x4x4 hyperblock, so for some versions Y will be ignored and for some precision of biome +setting is less than 1x1x1 block. + +Throws `unknown_biome` if the `biome_name` doesn't exist. + +### `update(pos)` + +Causes a block update at position. + +### `block_tick(pos)` + +Causes a block to tick at position. + +### `random_tick(pos)` + +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 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. + +Without item context it returns `false` if failed to destroy the block and `true` if block breaking was successful. +In item context, `true` means that breaking item has no nbt to use, `null` indicating that the tool should be +considered broken in process, and `nbt` type value, for a resulting NBT tag on a hypothetical tool. Its up to the +programmer to use that nbt to apply it where it belong + +Throws `unknown_item` if `tool` doesn't exist. + +Here is a sample code that can be used to mine blocks using items in player inventory, without using player context +for mining. Obviously, in this case the use of `harvest` would be much more applicable: + +
+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'
+  [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?');
+)
+
+ +### `harvest(player, pos)` + +Causes a block to be harvested by a specified player entity. Honors player item enchantments, as well as damages the +tool if applicable. If the entity is not a valid player, no block gets destroyed. If a player is not allowed to break +that block, a block doesn't get destroyed either. + +### `create_explosion(pos, power?, mode?, fire?, source?, attacker?)` + +Creates an explosion at a given position. Parameters work as follows: + - `'power'` - how strong the blast is, negative values count as 0 (default: `4` (TNT power)) + - `'mode'` - how to deal with broken blocks: `keep` keeps them, `destroy` destroys them and drops items, and `destroy_with_decay` destroys them, but doesn't always drop the items (default: `destroy_with_decay`) + - `fire` - whether extra fire blocks should be created (default: `false`) + - `source` - entity that is exploding. Note that it will not take explosion damage from this explosion (default: `null`) + - `attacker` - entity responsible for triggering, this will be displayed in death messages, and count towards kill counts, and can be damaged by the explosion (default: `null`) +Explosions created with this endpoint cannot be captured with `__on_explosion` event, however they will be captured +by `__on_explosion_outcome`. + +### `weather()`,`weather(type)`,`weather(type, ticks)` + +If called with no args, returns `'clear'`, `'rain` or `'thunder'` based on the current weather. If thundering, will +always return `'thunder'`, if not will return `'rain'` or `'clear'` based on the current weather. + +With one arg, (either `'clear'`, `'rain` or `'thunder'`), returns the number of remaining ticks for that weather type. +NB: It can thunder without there being a thunderstorm; there has to be both rain and thunder to form a storm. So if +running `weather()` returns `'thunder'`, you can use `weather('rain')>0` to see if there's a storm going on. + +With two args, sets the weather to the given `type` for `ticks` ticks. + +## Block and World querying + +### `pos(block), pos(entity)` + +Returns a triple of coordinates of a specified block or entity. Technically entities are queried with `query` function +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)) => [0,5,0]
+pos(player()) => [12.3, 45.6, 32.05]
+pos(block('stone')) => Error: Cannot fetch position of an unrealized block
+
+ +### `pos_offset(pos, direction, amount?)` + +Returns a coords triple that is offset in a specified `direction` by `amount` of blocks. The default offset amount is +1 block. To offset into opposite facing, use negative numbers for the `amount`. + +
+pos_offset(block(0,5,0), 'up', 2)  => [0,7,0]
+pos_offset([0,5,0], 'up', -2 ) => [0,3,0]
+
+ +### `(Deprecated) block_properties(pos)` + +Deprecated by `keys(block_state(pos))`. + +### `(Deprecated) property(pos, name)` + +Deprecated by `block_state(pos, name)` + +### `block_state(block)`, `block_state(block, property)` + +If used with a `block` argument only, it returns a map of block properties and their values. If a block has no properties, returns an +empty map. + +If `property` is specified, returns a string value of that property, or `null` if property is not applicable. + +Returned values or properties are always strings. It is expected from the user to know what to expect and convert +values to numbers using `number()` function or booleans using `bool()` function. Returned string values can be directly used +back in state definition in various applications where block properties are required. + +`block_state` can also accept block names as input, returning block's default state. + +Throws `unknown_block` if the provided input is not valid. + +
+set(x,y,z,'iron_block'); block_state(x,y,z)  => {}
+set(x,y,z,'iron_trapdoor','half','top'); block_state(x,y,z)  => {waterlogged: false, half: top, open: false, ...}
+set(x,y,z,'iron_trapdoor','half','top'); block_state(x,y,z,'half')  => top
+block_state('iron_trapdoor','half')  => top
+set(x,y,z,'air'); block_state(x,y,z,'half')  => null
+block_state(block('iron_trapdoor[half=top]'),'half')  => top
+block_state(block('iron_trapdoor[half=top]'),'powered')  => false
+bool(block_state(block('iron_trapdoor[half=top]'),'powered'))  => 0
+
+ +### `block_list()`, `block_list(tag)` + +Returns list of all blocks in the game. If `tag` is provided, returns list of all blocks that belong to this block tag. +
+block_list() => [dark_oak_button, wall_torch, structure_block, polished_blackstone_brick_slab, cherry_sapling... ]
+block_list('impermeable') => [glass, white_stained_glass, orange_stained_glass, magenta_stained_glass... ] //All da glass
+block_list('rails') => [rail, powered_rail, detector_rail, activator_rail]
+block_list('not_a_valid_block_tag') => null //Not a valid block tag
+
+ + +### `block_tags()`, `block_tags(block)`, `block_tags(block, tag)` + +Without arguments, returns list of available tags, with block supplied (either by coordinates, or via block name), returns lost +of tags the block belongs to, and if a tag is specified, returns `null` if tag is invalid, `false` if this block doesn't belong +to this tag, and `true` if the block belongs to the tag. + +Throws `unknown_block` if `block` doesn't exist + +
+block_tags() => [geode_invalid_blocks, wall_post_override, ice, wooden_stairs, bamboo_blocks, stone_bricks... ]
+block_tags('iron_block') => [mineable/pickaxe, needs_stone_tool, beacon_base_blocks]
+block_tags('glass') => [impermeable]
+block_tags('glass', 'impermeable') => true
+block_tags('glass', 'beacon_base_blocks') => false
+
+ +### `block_data(pos)` + +Return NBT string associated with specific location, or null if the block does not carry block data. Can be currently +used to match specific information from it, or use it to copy to another block + +
+block_data(x,y,z) => '{TransferCooldown:0,x:450,y:68, ... }'
+
+ +### `poi(pos), poi(pos, radius?, type?, status?, column_search?)` + +Queries a POI (Point of Interest) at a given position, returning `null` if none is found, or tuple of poi type and its +occupancy load. With optional `type`, `radius` and `status`, returns a list of POIs around `pos` within a +given `radius`. If the `type` is specified, returns only poi types of that types, or everything if omitted or `'any'`. +If `status` is specified (either `'any'`, `'available'`, or `'occupied'`) returns only POIs with that status. +With `column_search` set to `true`, it will return all POIs in a cuboid with `radius` blocks away on x and z, in the entire +block column from 0 to 255. Default (`false`) returns POIs within a spherical area centered on `pos` and with `radius` +radius. + +All results of `poi` calls are returned in sorted order with respect to the euclidean distance to the requested center of `pos`. + +The return format of the results is a list of poi type, occupancy load, and extra triple of coordinates. + +Querying for POIs using the radius is the intended use of POI mechanics, and the ability of accessing individual POIs +via `poi(pos)` in only provided for completeness. + +
+poi(x,y,z) => null  // nothing set at position
+poi(x,y,z) => ['meeting',3]  // its a bell-type meeting point occupied by 3 villagers
+poi(x,y,z,5) => []  // nothing around
+poi(x,y,z,5) => [['nether_portal',0,[7,8,9]],['nether_portal',0,[7,9,9]]] // two portal blocks in the range
+
+ +### `biome()` `biome(name)` `biome(block)` `biome(block/name, feature)`, `biome(noise_map)` + +Without arguments, returns the list of biomes in the world. + +With block, or name, returns the name of the biome in that position, or throws `'unknown_biome'` if provided biome or block are not valid. + +(1.18+) if passed a map of `continentalness`, `depth`, `erosion`, `humidity`, `temperature`, `weirdness`, returns the biome that exists at those noise values. +Note: Have to pass all 6 of the mentioned noise types and only these noise types for it to evaluate a biome. + +With an optional feature, it returns value for the specified attribute for that biome. Available and queryable features include: +* `'top_material'`: unlocalized block representing the top surface material (1.17.1 and below only) +* `'under_material'`: unlocalized block representing what sits below topsoil (1.17.1 and below only) +* `'category'`: the parent biome this biome is derived from. Possible values include (1.18.2 and below only): +`'none'`, `'taiga'`, `'extreme_hills'`, `'jungle'`, `'mesa'`, `'plains'`, `'savanna'`, +`'icy'`, `'the_end'`, `'beach'`, `'forest'`, `'ocean'`, `'desert'`, `'river'`, +`'swamp'`, `'mushroom'` , `'nether'`, `'underground'` (1.18+) and `'mountain'` (1.18+). +* `'tags'`: list of biome tags associated with this biome +* `'temperature'`: temperature from 0 to 1 +* `'fog_color'`: RGBA color value of fog +* `'foliage_color'`: RGBA color value of foliage +* `'sky_color'`: RGBA color value of sky +* `'water_color'`: RGBA color value of water +* `'water_fog_color'`: RGBA color value of water fog +* `'humidity'`: value from 0 to 1 indicating how wet is the biome +* `'precipitation'`: `'rain'` `'snot'`, or `'none'`... ok, maybe `'snow'`, but that means snots for sure as well. +* `'depth'`: (1.17.1 and below only) float value indicating how high or low the terrain should generate. Values > 0 indicate generation above sea level +and values < 0, below sea level. +* `'scale'`: (1.17.1 and below only) float value indicating how flat is the terrain. +* `'features'`: list of features that generate in the biome, grouped by generation steps +* `'structures'`: (1.17.1 and below only) list of structures that generate in the biome. + +### `solid(pos)` + +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. + +### `liquid(pos)` + +Boolean function, true if the block is liquid, or waterlogged (with any liquid). + +### `flammable(pos)` + +Boolean function, true if the block is flammable. + +### `transparent(pos)` + +Boolean function, true if the block is transparent. + +### `opacity(pos)` + +Numeric function, returning the opacity level of a block. + +### `blocks_daylight(pos)` + +Boolean function, true if the block blocks daylight. + +### `emitted_light(pos)` + +Numeric function, returning the light level emitted from the block. + +### `light(pos)` + +Numeric function, returning the total light level at position. + +### `block_light(pos)` + +Numeric function, returning the block light at position (from torches and other light sources). + +### `sky_light(pos)` + +Numeric function, returning the sky light at position (from sky access). + +### `effective_light(pos)` + +Numeric function, returning the "real" light at position, which is affected by time and weather. which also affects mobs spawning, frosted ice blocks melting. + +### `see_sky(pos)` + +Boolean function, returning true if the block can see sky. + +### `hardness(pos)` + +Numeric function, indicating hardness of a block. + +### `blast_resistance(pos)` + +Numeric function, indicating blast_resistance of a block. + +### `in_slime_chunk(pos)` + +Boolean indicating if the given block position is in a slime chunk. + +### `top(type, pos)` + +Returns the Y value of the topmost block at given x, z coords (y value of a block is not important), according to the +heightmap specified by `type`. Valid options are: + +* `light`: topmost light blocking block (1.13 only) +* `motion`: topmost motion blocking block +* `terrain`: topmost motion blocking block except leaves +* `ocean_floor`: topmost non-water block +* `surface`: topmost surface block + +
+top('motion', x, y, z)  => 63
+top('ocean_floor', x, y, z)  => 41
+
+ +### `suffocates(pos)` + +Boolean function, true if the block causes suffocation. + +### `power(pos)` + +Numeric function, returning redstone power level at position. + +### `ticks_randomly(pos)` + +Boolean function, true if the block ticks randomly. + +### `blocks_movement(pos)` + +Boolean function, true if the block at position blocks movement. + +### `block_sound(pos)` + +Returns the name of sound type made by the block at position. One of: + +`'wood'`, `'gravel'`, `'grass'`, `'stone'`, `'metal'`, `'glass'`, `'wool'`, `'sand'`, `'snow'`, +`'ladder'`, `'anvil'`, `'slime'`, `'sea_grass'`, `'coral'`, `'bamboo'`', `'shoots'`', `'scaffolding'`', `'berry'`', `'crop'`', +`'stem'`', `'wart'`', +`'lantern'`', `'fungi_stem'`', `'nylium'`', `'fungus'`', `'roots'`', `'shroomlight'`', `'weeping_vines'`', `'soul_sand'`', + `'soul_soil'`', `'basalt'`', +`'wart'`', `'netherrack'`', `'nether_bricks'`', `'nether_sprouts'`', `'nether_ore'`', `'bone'`', `'netherite'`', `'ancient_debris'`', +`'lodestone'`', `'chain'`', `'nether_gold_ore'`', `'gilded_blackstone'`', +`'candle'`', `'amethyst'`', `'amethyst_cluster'`', `'small_amethyst_bud'`', `'large_amethyst_bud'`', `'medium_amethyst_bud'`', +`'tuff'`', `'calcite'`', `'copper'`' + +### `(Deprecated) material(pos)` + +Returns `'unknown'`. The concept of material for blocks is removed. On previous versions it returned the name of the material the block +was made of. + +### `map_colour(pos)` + +Returns the map colour of a block at position. One of: + +`'air'`, `'grass'`, `'sand'`, `'wool'`, `'tnt'`, `'ice'`, `'iron'`, `'foliage'`, `'snow'`, `'clay'`, `'dirt'`, +`'stone'`, `'water'`, `'wood'`, `'quartz'`, `'adobe'`, `'magenta'`, `'light_blue'`, `'yellow'`, `'lime'`, `'pink'`, +`'gray'`, `'light_gray'`, `'cyan'`, `'purple'`, `'blue'`, `'brown'`, `'green'`, `'red'`, `'black'`, `'gold'`, +`'diamond'`, `'lapis'`, `'emerald'`, `'obsidian'`, `'netherrack'`, `'white_terracotta'`, `'orange_terracotta'`, +`'magenta_terracotta'`, `'light_blue_terracotta'`, `'yellow_terracotta'`, `'lime_terracotta'`, `'pink_terracotta'`, +`'gray_terracotta'`, `'light_gray_terracotta'`, `'cyan_terracotta'`, `'purple_terracotta'`, `'blue_terracotta'`, +`'brown_terracotta'`, `'green_terracotta'`, `'red_terracotta'`, `'black_terracotta'`, +`'crimson_nylium'`, `'crimson_stem'`, `'crimson_hyphae'`, `'warped_nylium'`, `'warped_stem'`, `'warped_hyphae'`, `'warped_wart'` + +### `sample_noise()`, `sample_noise(pos, ... types?)` 1.18+ + +Samples the world generation noise values / data driven density function(s) at a given position. + +If no types are passed in, or no arguments are given, it returns a list of all the available registry defined density functions. + +With a single function name passed in, it returns a scalar. With multiple function names passed in, it returns a list of results. + +Function accepts any registry defined density functions, both built in, as well as namespaced defined in datapacks. +On top of that, scarpet provides the following list of noises sampled directly from the current level (and not returned with no-argument call): + + +`'barrier_noise'`, `'fluid_level_floodedness_noise'`, `'fluid_level_spread_noise'`, `'lava_noise'`, +`'temperature'`, `'vegetation'`, `'continents'`, `'erosion'`, `'depth'`, `'ridges'`, +`'initial_density_without_jaggedness'`, `'final_density'`, `'vein_toggle'`, `'vein_ridged'` and `'vein_gap'` + +
+// requesting single value
+sample_density(pos, 'continents') => 0.211626790923
+// passing type as multiple arguments
+sample_density(pos, 'continents', 'depth', 'overworld/caves/pillars', 'mydatapack:foo/my_function') => [-0.205013844481, 1.04772473438, 0.211626790923, 0.123]
+
+ +### `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 an advantage. Vanilla `fill/clone` +commands only check the specified corners for loadness. + +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(player()))  => 1
+loaded(100000,100,1000000)  => 0
+
+ +### `(Deprecated) loaded_ep(pos)` + +Boolean function, true if the block is loaded and entity processing, as per 1.13.2 + +Deprecated as of scarpet 1.6, use `loaded_status(x) > 0`, or just `loaded(x)` with the same effect + +### `loaded_status(pos)` + +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)` + +Returns `true` if the region file for the chunk exists, +`false` otherwise. If optional force is `true` it will also check if the chunk has a non-empty entry in its region file +Can be used to assess if the chunk has been touched by the game or not. + +`generation_status(pos, false)` only works on currently loaded chunks, and `generation_status(pos, true)` will create +an empty loaded chunk, even if it is not needed, so `is_chunk_generated` can be used as a efficient proxy to determine +if the chunk physically exists. + +Running `is_chunk_generated` is has no effects on the world, but since it is an external file operation, it is +considerably more expensive (unless area is loaded) than other generation and loaded checks. + +### `generation_status(pos), generation_status(pos, true)` + +Returns generation status as per the ticket system. Can return any value from several available but chunks +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) + +### `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([pos, ...])` +Removes and resets the chunk, all chunks in the specified area or all chunks in a list at once, removing all previous +blocks and entities, and replacing it with a new generation. For all currently loaded chunks, they will be brought +to their current generation status, and updated to the player. All chunks that are not in the loaded area, will only +be generated to the `'structure_starts'` status, allowing to generate them fully as players are visiting them. +Chunks in the area that has not been touched yet by the game will not be generated / regenerated. + +It returns a `map` with a report indicating how many chunks were affected, and how long each step took: + * `requested_chunks`: total number of chunks in the requested area or list + * `affected_chunks`: number of chunks that will be removed / regenerated + * `loaded_chunks`: number of currently loaded chunks in the requested area / list + * `relight_count`: number of relit chunks + * `relight_time`: time took to relit chunks + * `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. + +## Structure and World Generation Features API + +Scarpet provides convenient methods to access and modify information about structures as well as spawn in-game +structures and features. List of available options and names that you can use depends mostly if you are using scarpet +with minecraft 1.16.1 and below or 1.16.2 and above since in 1.16.2 Mojang has added JSON support for worldgen features +meaning that since 1.16.2 - they have official names that can be used by datapacks and scarpet. If you have most recent +scarpet on 1.16.4, you can use `plop()` to get all available worldgen features including custom features and structures +controlled by datapacks. It returns a map of lists in the following categories: +`'scarpet_custom'`, `'configured_features'`, `'structures'`, `'features'`, `'structure_types'` + +### Previous Structure Names, including variants (for MC1.16.1 and below only) +* `'monument'`: Ocean Monument. Generates at fixed Y coordinate, surrounds itself with water. +* `'fortress'`: Nether Fortress. Altitude varies, but its bounded by the code. +* `'mansion'`: Woodland Mansion +* `'jungle_temple'`: Jungle Temple +* `'desert_temple'`: Desert Temple. Generates at fixed Y altitude. +* `'endcity'`: End City with Shulkers (in 1.16.1- as `'end_city`) +* `'igloo'`: Igloo +* `'shipwreck'`: Shipwreck +* `'shipwreck2'`: Shipwreck, beached +* `'witch_hut'` +* `'ocean_ruin'`, `ocean_ruin_small'`, `ocean_ruin_tall'`: Stone variants of ocean ruins. +* `'ocean_ruin_warm'`, `ocean_ruin_warm_small'`, `ocean_ruin_warm_tall'`: Sandstone variants of ocean ruins. +* `'treasure'`: A treasure chest. Yes, its a whole structure. +* `'pillager_outpost'`: A pillager outpost. +* `'mineshaft'`: A mineshaft. +* `'mineshaft_mesa'`: A Mesa (Badlands) version of a mineshaft. +* `'village'`: Plains, oak village. +* `'village_desert'`: Desert, sandstone village. +* `'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 Names (1.16.1 and below) + +* `'oak'` +* `'oak_beehive'`: oak with a hive (1.15+). +* `'oak_large'`: oak with branches. +* `'oak_large_beehive'`: oak with branches and a beehive (1.15+). +* `'birch'` +* `'birch_large'`: tall variant of birch tree. +* `'shrub'`: low bushes that grow in jungles. +* `'shrub_acacia'`: low bush but configured with acacia (1.14 only) +* `'shrub_snowy'`: low bush with white blocks (1.14 only) +* `'jungle'`: a tree +* `'jungle_large'`: 2x2 jungle tree +* `'spruce'` +* `'spruce_large'`: 2x2 spruce tree +* `'pine'`: spruce with minimal leafage (1.15+) +* `'pine_large'`: 2x2 spruce with minimal leafage (1.15+) +* `'spruce_matchstick'`: see 1.15 pine (1.14 only). +* `'spruce_matchstick_large'`: see 1.15 pine_large (1.14 only). +* `'dark_oak'` +* `'acacia'` +* `'oak_swamp'`: oak with more leaves and vines. +* `'well'`: desert well +* `'grass'`: a few spots of tall grass +* `'grass_jungle'`: little bushier grass feature (1.14 only) +* `'lush_grass'`: grass with patchy ferns (1.15+) +* `'tall_grass'`: 2-high grass patch (1.15+) +* `'fern'`: a few random 2-high ferns +* `'cactus'`: random cacti +* `'dead_bush'`: a few random dead bushi +* `'fossils'`: underground fossils, placement little wonky +* `'mushroom_brown'`: large brown mushroom. +* `'mushroom_red'`: large red mushroom. +* `'ice_spike'`: ice spike. Require snow block below to place. +* `'glowstone'`: glowstone cluster. Required netherrack above it. +* `'melon'`: a patch of melons +* `'melon_pile'`: a pile of melons (1.15+) +* `'pumpkin'`: a patch of pumpkins +* `'pumpkin_pile'`: a pile of pumpkins (1.15+) +* `'sugarcane'` +* `'lilypad'` +* `'dungeon'`: Dungeon. These are hard to place, and fail often. +* `'iceberg'`: Iceberg. Generate at sea level. +* `'iceberg_blue'`: Blue ice iceberg. +* `'lake'` +* `'lava_lake'` +* `'end_island'` +* `'chorus'`: Chorus plant. Require endstone to place. +* `'sea_grass'`: a patch of sea grass. Require water. +* `'sea_grass_river'`: a variant. +* `'kelp'` +* `'coral_tree'`, `'coral_mushroom'`, `'coral_claw'`: various coral types, random color. +* `'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) + + +### World Generation Features and Structures (as of MC1.16.2+) + +Use `plop():'structure_types'`, `plop():'structures'`, `plop():'features'`, and `plop():'configured_features'` for a list of available options. Your output may vary based on +datapacks installed in your world. + +### Custom Scarpet Features + +Use `plop():'scarpet_custom'` for a full list. + +These contain some popular features and structures that are impossible or difficult to obtain with vanilla structures/features. + +* `'bastion_remnant_bridge'` - Bridge version of a bastion remnant +* `'bastion_remnant_hoglin_stable'` - Hoglin stables version of a bastion remnant +* `'bastion_remnant_treasure'` - Treasure version of a bastion remnant +* `'bastion_remnant_units'` - Housing units version of a bastion remnant +* `'birch_bees'` - birch tree that always generates with a beehive unlike standard that generate with probability +* `'coral'` - random standalone coral feature, typically part of `'warm_ocean_vegetation'` +* `'coral_claw'` - claw coral feature +* `'coral_mushroom'` - mushroom coral feature +* `'coral_tree'` - tree coral feature +* `'fancy_oak_bees'` - large oak tree variant with a mandatory beehive unlike standard that generate with probability +* `'oak_bees'` - normal oak tree with a mandatory beehive unlike standard that generate with probability + + +### `structure_eligibility(pos, ?structure, ?size_needed)` + +Checks worldgen eligibility for a structure in a given chunk. Requires a `Structure Variant` name (see above), +or `Standard Structure` to check structures of this type. +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 or invalid, `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, or a `Standard Structure` was specified, like `'village'`,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. + +Throws `unknown_structure` if structure doesn't exist. + +### `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 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. If structure is invalid, its data will be `null`. + +Requires a `Standard Structure` name (see above). + +### `structure_references(pos), structure_references(pos, structure_name)` + +Returns structure information that a chunk with a given block position is part of. `structure_references` function +can be called with a block, or a block and a structure name. In the first case it returns a list of structure names +that give chunk belongs to. When called with an extra structure name, returns list of positions pointing to the +lowest block position in chunks that hold structure starts for these structures. You can query that chunk structures +then to get its bounding boxes. + +Requires a `Standard Structure` name (see above). + +### `set_structure(pos, structure_name), set_structure(pos, structure_name, null)` + +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 the structure a given point is in, use `structure_references` to find where current +structure starts. + +Requires a `Structure Variant` or `Standard Structure` name (see above). If standard name is used, the variant of the +structure may depend on the biome, otherwise the default structure for this type will be generated. + +Throws `unknown_structure` if structure doesn't exist. + +### `plop(pos, what)` + +Plops a structure or a feature at a given `pos`, so block, triple position coordinates or a list of coordinates. +To `what` gets plopped and exactly where it often depends on the feature or structure itself. + +Requires a `Structure Type`, `Structure`, `World Generation Feature` or `Custom Scarpet Feature` name (see +above). If standard name is used, the variant of the structure may depend on the biome, otherwise the default +structure for this type will be generated. + +All structures are chunk aligned, and often span multiple chunks. Repeated calls to plop a structure in the same chunk +would result either in the same structure generated on top of each other, or with different state, but same position. +Most structures generate at specific altitudes, which are hardcoded, or with certain blocks around them. API will +cancel all extra position / biome / random requirements for structure / feature placement, but some hardcoded +limitations may still cause some of structures/features not to place. Some features require special blocks to be +present, like coral -> water or ice spikes -> snow block, and for some features, like fossils, placement is all sorts +of messed up. This can be partially avoided for structures by setting their structure information via `set_structure`, +which sets it without looking into world blocks, and then use `plop` to fill it with blocks. This may, or may not work. + +All generated structures will retain their properties, like mob spawning, however in many cases the world / dimension +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. diff --git a/docs/scarpet/api/Entities.md b/docs/scarpet/api/Entities.md new file mode 100644 index 0000000..6d37ea3 --- /dev/null +++ b/docs/scarpet/api/Entities.md @@ -0,0 +1,1002 @@ +# Entity API + +## Entity Selection + +Entities have to be fetched before using them. Entities can also change their state between calls to the script if +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. +For player-scoped apps (which is a default) its always the owning player or `null` if it not present even if some code +still runs in their name. +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, +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 he/she is logged in. + +### `entity_id(uuid), entity_id(id)` + +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. + +### `entity_list(descriptor)` + +Returns global lists of entities in the current dimension matching specified descriptor. +Calls to `entity_list` always fetch entities from the current world that the script executes. + +### `entity_types(descriptor)` + +Resolves a given descriptor returning list of entity types that match it. The returned list of types is also a valid list +of descriptors that can be use elsewhere where entity types are required. + +Currently, the following descriptors are available: + +* `*`: all entities, even `!valid`, matches all entity types. +* `valid` - all entities that are not dead (health > 0). All main categories below also return only +entities in the `valid` category. matches all entity types. `!valid` matches all entites that are already dead of all types. +* `living` - all entities that resemble a creature of some sort +* `projectile` - all entities or types that are not living that can be throw or projected, `!projectile` matches all types that + are not living, but cannot the thrown or projected. +* `minecarts` matches all minecart types. `!minecarts` matches all types that are not live, but also not minecarts. Using plural +since `minecart` is a proper entity type on its own. +* `undead`, `arthropod`, `aquatic`, `regular`, `illager` - all entities / types that belong to any of these groups. All +living entities belong to one and only one of these. Corresponding negative (e.g. `!undead`) corresponds to all mobs that are +living but don't belong to that group. Entity groups are used in interaction / battle mechanics like smite for undead, or impaling +for aquatic. Also certain mechanics interact with groups, like ringing a bell with illagers. All other mobs that don't have any of these traits belong +to the `regular` group. +* `monster`, `creature`, `ambient`, `water_creature`, `water_ambient`, `misc` - another categorization of +living entities based on their spawn group. Negative descriptor resolves to all living types that don't belong to that +category. +* All entity tags including those provided with datapacks. Built-in entity tags include: `skeletons`, `raiders`, +`beehive_inhabitors` (bee, duh), `arrows` and `impact_projectiles`. +* Any of the standard entity types, equivalent to selection from `/summon` vanilla command, which is one of the options returned +by `entity_types()`, except for `'fishing_bobber'` and `'player'`. + +All categories can be preceded with `'!'` which will fetch all entities (unless otherwise noted) that are valid (health > 0) but not +belonging to that group. + +### `entity_area(type, center, distance)` + + +Returns entities of a specified type in an area centered on `center` and at most `distance` blocks away from +the center point/area. Uses the same `type` selectors as `entities_list`. + +`center` and `distance` can either be a triple of coordinates or three consecutive arguments for `entity_area`. `center` can +also be represented as a block, in this case the search box will be centered on the middle of the block, or an entity - in this case +entire bounding box of the entity serves as a 'center' of search which is then expanded in all directions with the `'distance'` vector. + +In any case - returns all entities which bounding box collides with the bounding box defined by `'center'` and `'distance'`. + +entity_area is simpler than `entity_selector` and runs about 20% faster, but is limited to predefined selectors and +cuboid search area. + +### `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 it should be as fast as other methods of selecting entities. Unlike other +entities fetching / filtering method, this one doesn't guarantee to return entities from current dimension, since +selectors can return any loaded entity in the world. + +### `spawn(name, pos, nbt?)` + +Spawns and places an entity in world, like `/summon` vanilla command. Requires a position to spawn, and optional +extra nbt data to merge with the entity. What makes it different from calling `run('summon ...')`, is the fact that +you get the entity back as a return value, which is swell. + +## Entity Manipulation + +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: + +
+query(p, 'name') <=> p ~ 'name'     // much shorter and cleaner
+query(p, 'holds', 'offhand') <=> p ~ ['holds', 'offhand']    // not really but can be done
+
+ +### `query(e, 'removed')` + +Boolean. True if the entity is removed. + +### `query(e, 'id')` + +Returns numerical id of the entity. Most efficient way to keep track of entities 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). + +### `query(e, 'uuid')` + +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 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')` + +Triple of the entity's position + +### `query(e, 'location')` + +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')` + +Respective component of entity's coordinates + +### `query(e, 'pitch')`, `query(e, 'yaw')` + +Pitch and Yaw or where entity is looking. + +### `query(e, 'head_yaw')`, `query(e, 'body_yaw')` + +Applies to living entites. Sets their individual head and body facing angle. + +### `query(e, 'look')` + +Returns a 3d vector where the entity is looking. + +### `query(e, 'motion')` + +Triple of entity's motion vector, `[motion_x, motion_y, motion_z]`. Motion represents the velocity from all the forces +that exert on the given entity. Things that are not 'forces' like voluntary movement, or reaction from the ground are +not part of said forces. + +### `query(e, 'motion_x'), query(e, 'motion_y'), query(e, 'motion_z')` + +Respective component of the entity's motion vector + +### `query(e, 'on_ground')` + +Returns `true` if an entity is standing on firm ground and falling down due to that. + +### `query(e, 'name'), query(e, 'display_name'), query(e, 'custom_name'), query(e, 'type')` + +String of entity name or formatted text in the case of `display_name` + +
+query(e,'name')  => Leatherworker
+query(e,'custom_name')  => null
+query(e,'type')  => villager
+
+ +### `query(e, 'command_name')` + +Returns a valid string to be used in commands to address an entity. Its UUID for all entities except +player, where its their name. + +
+run('/kill ' + e~'command_name');
+
+ +### `query(e, 'persistence')` + +Returns if a mob has a persistence tag or not. Returns `null` for non-mob entities. + +### `query(e, 'is_riding')` + +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')` + +Entity that `e` rides. + +### `query(e, 'unmountable')` + +Boolean, true if the entity cannot be mounted. + +### `(deprecated) query(e, 'tags')` + +Deprecated by `query(e, 'scoreboard_tags')` + +### `query(e, 'scoreboard_tags')` + +List of entity's scoreboard tags. + +### `(deprecated) query(e, 'has_tag',tag)` + +Deprecated by `query(e, 'has_scoreboard_tag',tag)` + +### `query(e, 'has_scoreboard_tag',tag)` + +Boolean, true if the entity is marked with a `tag` scoreboad tag. + +### `query(e, 'entity_tags')` + +List of entity tags assigned to the type this entity represents. + +### `query(e, 'has_entity_tag', tag)` + +Returns `true` if the entity matches that entity tag, `false` if it doesn't, and `null` if the tag is not valid. + +### `query(e, 'is_burning')` + +Boolean, true if the entity is burning. + +### `query(e, 'fire')` + +Number of remaining ticks of being on fire. + +### `query(e, 'is_freezing')` + +Boolean, true if the entity is freezing. + +### `query(e, 'frost')` + +Number of remaining ticks of being frozen. + +### `query(e, 'silent')` + +Boolean, true if the entity is silent. + +### `query(e, 'gravity')` + +Boolean, true if the entity is affected by gravity, like most entities are. + +### `query(e, 'invulnerable')` + +Boolean, true if the entity is invulnerable. + +### `query(e, 'immune_to_fire')` + +Boolean, true if the entity is immune to fire. + +### `query(e, 'immune_to_frost')` + +Boolean, true if the entity is immune to frost. + +### `query(e, 'dimension')` + +Name of the dimension the entity is in. + +### `query(e, 'height')` + +Height of the entity in blocks. + +### `query(e, 'width')` + +Width of the entity in blocks. + +### `query(e, 'eye_height')` + +Eye height of the entity in blocks. + +### `query(e, 'age')` + +Age of the entity in ticks, i.e. how long it existed. + +### `query(e, 'breeding_age')` + +Breeding age of passive entity, in ticks. If negative, time to adulthood, if positive, breeding cooldown. + +### `query(e, 'despawn_timer')` + +For living entities, the number of ticks they fall outside of immediate player presence. + +### `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 or item frame entity, `null` otherwise. + +### `query(e, 'offering_flower')` + +Whether the given iron golem has a red flower in their hand. returns null for all other entities + + +### `query(e, 'blue_skull')` + +Whether the given wither skull entity is blue. returns null for all other entities + + +### `query(e, 'count')` + +Number of items in a stack from item entity, `null` otherwise. + +### `query(e, 'pickup_delay')` + +Retrieves pickup delay timeout for an item entity, `null` otherwise. + +### `query(e, 'is_baby')` + +Boolean, true if its a baby. + +### `query(e, 'target')` + +Returns mob's attack target or null if none or not applicable. + +### `query(e, 'home')` + +Returns creature's home position (as per mob's AI, leash etc) or null if none or not applicable. + +### `query(e, 'spawn_point')` + +Returns position tuple, dimension, spawn angle, and whether spawn is forced, assuming the player has a spawn position. +Returns `false` if spawn position is not set, and `null` if `e` is not a player. + +### `query(e, 'path')` + +Returns path of the entity if present, `null` otherwise. The path comprises of list of nodes, each is a list +of block value, node type, penalty, and a boolean indicated if the node has been visited. + +### `query(e, 'pose')` + +Returns a pose of an entity, one of the following options: + * `'standing'` + * `'fall_flying'` + * `'sleeping'` + * `'swimming'` + * `'spin_attack'` + * `'crouching'` + * `'dying'` + +### `query(e, 'sneaking')` + +Boolean, true if the entity is sneaking. + +### `query(e, 'sprinting')` + +Boolean, true if the entity is sprinting. + +### `query(e, 'swimming')` + +Boolean, true if the entity is swimming. + +### `query(e, 'jumping')` + +Boolean, true if the entity is jumping. + +### `query(e, 'swinging')` + +Returns `true` if the entity is actively swinging their hand, `false` if not and `null` if swinging is not applicable to +that entity. + +### `query(e, 'gamemode')` + +String with gamemode, or `null` if not a player. + +### `query(e, 'gamemode_id')` + +Good'ol gamemode id, or null if not a player. + +### `query(e, 'player_type')` + +Returns `null` if the argument is not a player, otherwise: + +* `singleplayer`: for singleplayer game +* `multiplayer`: for players on a dedicated server +* `lan_host`: for singleplayer owner that opened the game to LAN +* `lan_player`: for all other players that connected to a LAN host +* `fake`: any carpet-spawned fake player +* `shadow`: any carpet-shadowed real player +* `realms`: ? + +### `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')` + +Player's permission level, or `null` if not applicable for this entity. + +### `query(e, 'client_brand')` + +Returns recognized type of client of the client connected. Possible results include `'vanilla'`, or `'carpet '` where +version indicates the version of the connected carpet client. + +### `query(e, 'effect', name?)` + +Without extra arguments, it returns list of effect active on a living entity. Each entry is a triple of short +effect name, amplifier, and remaining duration in ticks (-1 if it has infinity duration). With an argument, if the living entity has not that potion active, +returns `null`, otherwise return a tuple of amplifier and remaining duration. + +
+query(p,'effect')  => [[haste, 0, 177], [speed, 0, 177]]
+query(p,'effect','haste')  => [0, 177]
+query(p,'effect','resistance')  => null
+
+ +### `query(e, 'health')` + +Number indicating remaining entity health, or `null` if not applicable. + +### `query(e, 'may_fly')` + +Returns a boolean indicating if the player can fly. + +### `query(e, 'flying')` + +Returns a boolean indicating if the player is flying. + +### `query(e, 'may_build')` + +Returns a boolean indicating if the player is allowed to place blocks. + +### `query(e, 'insta_build')` + +Returns a boolean indicating if the player can place blocks without consuming the item and if the player can shoot arrows without having them in the inventory. + +### `query(e, 'fly_speed')` + +Returns a number indicating the speed at which the player moves while flying. + +### `query(e, 'walk_speed')` + +Returns a number indicating the speed at which the player moves while walking. + +### `query(e, 'hunger')` +### `query(e, 'saturation')` +### `query(e, 'exhaustion')` + +Retrieves player hunger related information. For non-players, returns `null`. + +### `query(e, 'absorption')` + +Gets the absorption of the player (yellow hearts, e.g. when having a golden apple.) + +### `query(e, 'xp')` +### `query(e, 'xp_level')` +### `query(e, 'xp_progress')` +### `query(e, 'score')` + +Numbers related to player's xp. `xp` is the overall xp player has, `xp_level` is the levels seen in the hotbar, +`xp_progress` is a float between 0 and 1 indicating the percentage of the xp bar filled, and `score` is the number displayed upon death + +### `query(e, 'air')` + +Number indicating remaining entity air, or `null` if not applicable. + +### `query(e, 'language')` + +Returns `null` for any non-player entity, if not returns the player's language as a string. + +### `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` +* `head` +* `chest` +* `legs` +* `feet` + +If `slot` is not specified, it defaults to the main hand. + +### `query(e, 'selected_slot')` + +Number indicating the selected slot of entity's inventory. Currently only applicable to players. + +### `query(e, 'active_block')` + +Returns currently mined block by the player, as registered by the game server. + +### `query(e, 'breaking_progress')` + +Returns current breaking progress of a current player mining block, or `null`, if no block is mined. +Breaking progress, if not null, is any number 0 or above, while 10 means that the block should already be +broken by the client. This value may tick above 10, if the client / connection is lagging. + +Example: + +The following program provides custom breaking times, including nice block breaking animations, including instamine, for +blocks that otherwise would take longer to mine. + +[Video demo](https://youtu.be/zvEEuGxgCio) +```py +global_blocks = { + 'oak_planks' -> 0, + 'obsidian' -> 1, + 'end_portal_frame' -> 5, + 'bedrock' -> 10 +}; + +__on_player_clicks_block(player, block, face) -> +( + step = global_blocks:str(block); + if (step == 0, + destroy(block, -1); // instamine + , step != null, + schedule(0, '_break', player, pos(block), str(block), step, 0); + ) +); + +_break(player, pos, name, step, lvl) -> +( + current = player~'active_block'; + if (current != name || pos(current) != pos, + modify(player, 'breaking_progress', null); + , + modify(player, 'breaking_progress', lvl); + if (lvl >= 10, destroy(pos, -1)); + schedule(step, '_break', player, pos, name, step, lvl+1) + ); +) +``` + +### `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?...)` + +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 +action. This can be customized with `options`, use `'blocks'` to trace for blocks, `'liquids'` to include liquid blocks +as possible results, and `'entities'` to trace entities. You can also specify `'exact'` which returns the actual hit + coordinate as a triple, instead of a block or entity value. Any combination of the above is possible. When tracing +entities and blocks, blocks will take over the priority even if transparent or non-colliding +(aka fighting chickens in tall grass). + +Regardless of the options selected, the result could be: + - `null` if nothing is in reach + - an entity if look targets an entity + - block value if block is in reach, or + - a coordinate triple if `'exact'` option was used and hit was successful. + +### `query(e, 'attribute')` `query(e, 'attribute', name)` + +returns the value of an attribute of the living entity. If the name is not provided, +returns a map of all attributes and values of this entity. If an attribute doesn't apply to the entity, +or the entity is not a living entity, `null` is returned. + +### `query(e, 'brain', memory)` + +Retrieves brain memory for entity. Possible memory units highly depend on the game version. Brain is availalble +for villagers (1.15+) and Piglins, Hoglins, Zoglins and Piglin Brutes (1.16+). If memory is not present or +not available for the entity, `null` is returned. + +Type of the returned value (entity, position, number, list of things, etc) depends on the type of the requested +memory. On top of that, since 1.16, memories can have expiry - in this case the value is returned as a list of whatever +was there, and the current ttl in ticks. + +Available retrievable memories for 1.15.2: +* `home`, `job_site`, `meeting_point`, `secondary_job_site`, `mobs`, `visible_mobs`, `visible_villager_babies`, +`nearest_players`, `nearest_visible_player`, `walk_target`, `look_target`, `interaction_target`, +`breed_target`, `path`, `interactable_doors`, `opened_doors`, `nearest_bed`, `hurt_by`, `hurt_by_entity`, +`nearest_hostile`, `hiding_place`, `heard_bell_time`, `cant_reach_walk_target_since`, +`golem_last_seen_time`, `last_slept`, `last_woken`, `last_worked_at_poi` + +Available retrievable memories as of 1.16.2: +* `home`, `job_site`, `potential_job_site`, `meeting_point`, `secondary_job_site`, `mobs`, `visible_mobs`, +`visible_villager_babies`, `nearest_players`, `nearest_visible_players`, `nearest_visible_targetable_player`, +`walk_target`, `look_target`, `attack_target`, `attack_cooling_down`, `interaction_target`, `breed_target`, +`ride_target`, `path`, `interactable_doors`, `opened_doors`, `nearest_bed`, `hurt_by`, `hurt_by_entity`, `avoid_target`, +`nearest_hostile`, `hiding_place`, `heard_bell_time`, `cant_reach_walk_target_since`, `golem_detected_recently`, +`last_slept`, `last_woken`, `last_worked_at_poi`, `nearest_visible_adult`, `nearest_visible_wanted_item`, +`nearest_visible_nemesis`, `angry_at`, `universal_anger`, `admiring_item`, `time_trying_to_reach_admire_item`, +`disable_walk_to_admire_item`, `admiring_disabled`, `hunted_recently`, `celebrate_location`, `dancing`, +`nearest_visible_huntable_hoglin`, `nearest_visible_baby_hoglin`, `nearest_targetable_player_not_wearing_gold`, +`nearby_adult_piglins`, `nearest_visible_adult_piglins`, `nearest_visible_adult_hoglins`, +`nearest_visible_adult_piglin`, `nearest_visible_zombiefied`, `visible_adult_piglin_count`, +`visible_adult_hoglin_count`, `nearest_player_holding_wanted_item`, `ate_recently`, `nearest_repellent`, `pacified` + + +### `query(e, 'nbt', path?)` + +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 +be used when there is no other option. Returned value is of type `nbt`, which can be further manipulated with nbt +type objects via `get, put, has, delete`, so try to use API calls first for that. + +## Entity Modification + +Like with entity querying, entity modifications happen through one function. + +### `modify(e, 'remove')` + +Removes (not kills) entity from the game. + +### `modify(e, 'kill')` + +Kills the entity. + +### `modify(e, 'pos', x, y, z), modify(e, 'pos', [x,y,z] )` + +Moves the entity to a specified coords. + +### `modify(e, 'location', x, y, z, yaw, pitch), modify(e, 'location', [x, y, z, yaw, pitch] )` + +Changes full location vector all at once. + +### `modify(e, 'x', x), modify(e, 'y', y), modify(e, 'z', z)` + +Changes entity's location in the specified direction. + +### `modify(e, 'pitch', angle), modify(e, 'yaw', angle)` + +Changes entity's pitch or yaw angle. + +### `modify(e, 'look', x, y, z), modify(e, 'look', [x,y,z] )` + +Sets entity's 3d vector where the entity is looking. +For cases where the vector has a length of 0, yaw and pitch won't get changed. +When pointing straight up or down, yaw will stay the same. + +### `modify(e, 'head_yaw', angle)`, `modify(e, 'body_yaw', angle)` + +For living entities, controls their head and body yaw angle. + +### `modify(e, 'move', x, y, z), modify(e, 'move', [x,y,z] )` + +Moves the entity by a vector from its current location. + +### `modify(e, 'motion', x, y, z), modify(e, 'motion', [x,y,z] )` + +Sets the motion vector (where and how much entity is moving). + +### `modify(e, 'motion_x', x), modify(e, 'motion_y', y), modify(e, 'motion_z', z)` + +Sets the corresponding component of the motion vector. + +### `modify(e, 'accelerate', x, y, z), modify(e, 'accelerate', [x, y, z] )` + +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', name, visible)` + +Sets the custom name of the entity. Without arguments - clears current custom name. Optional visible affects +if the custom name is always visible, even through blocks. + +### `modify(e, 'persistence', bool?)` + +Sets the entity persistence tag to `true` (default) or `false`. Only affects mobs. Persistent mobs +don't despawn and don't count towards the mobcap. + +### `modify(e, 'item', item_triple)` + +Sets the item for the item or item frame entity. (The item triple is a list of `[item_name, count, nbt]`, or just an item name.) + +### `modify(e, 'offering_flower', bool)` + +Sets if the iron golem has a red flower in hand. + +### `modify(e, 'blue_skull', bool)` + +Sets whether the wither skull entity is blue. + +### `modify(e, 'age', number)` + +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. + +### `modify(e, 'mount', other)` + +Mounts the entity to the `other`. + +### `modify(e, 'unmountable', boolean)` + +Denies or allows an entity to be mounted. + +### `modify(e, 'drop_passengers')` + +Shakes off all passengers. + +### `modify(e, 'mount_passengers', passenger, ? ...), modify(e, 'mount_passengers', [passengers] )` + +Mounts on all listed entities on `e`. + +### `modify(e, 'tag', tag, ? ...), modify(e, 'tag', [tags] )` + +Adds tag(s) to the entity. + +### `modify(e, 'clear_tag', tag, ? ...), modify(e, 'clear_tag', [tags] )` + +Removes tag(s) from the entity. + +### `modify(e, 'talk')` + +Make noises. + +### `modify(e, 'ai', boolean)` + +If called with `false` value, it will disable AI in the mob. `true` will enable it again. + +### `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. + +### `modify(e, 'effect', name?, duration?, amplifier?, show_particles?, show_icon?, ambient?)` + +Applies status effect to the living entity. Takes several optional parameters, which default to `0`, `true`, +`true` and `false`. If no duration is specified, or if it's null or 0, the effect is removed. If duration is less than 0, it will represent infinity. If name is not specified, +it clears all effects. + +### `modify(e, 'health', float)` + +Modifies the health of an entity. + +### `modify(e, 'may_fly', boolean)` + +Allows or denies the player the ability to fly. If the player is flying and the ability is removed, the player will stop flying. + +### `modify(e, 'flying', boolean)` + +Changes the flight status of the player (if it is flying or not). + +### `modify(e, 'may_build', boolean)` + +Allows or denies the player the ability to place blocks. + +### `modify(e, 'insta_build', boolean)` + +Allows or denies the player to place blocks without reducing the item count of the used stack and to shoot arrows without having them in the inventory. + +### `modify(e, 'fly_speed', float)` + +Modifies the value of the speed at which the player moves while flying. + +### `modify(e, 'walk_speed', float)` + +Modifies the value of the speed at which the player moves while walking. + +### `modify(e, 'selected_slot', int)` + +Changes player's selected slot. + +### `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, 'spawn_point')`, `modify(e, 'spawn_point', null)`, `modify(e, 'spawn_point', pos, dimension?, angle?, forced?)` + +Changes player respawn position to given position, optional dimension (defaults to current player dimension), angle (defaults to +current player facing) and spawn forced/fixed (defaults to `false`). If `none` or nothing is passed, the respawn point +will be reset (as removed) instead. + +### `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, 'swing')` `modify(e, 'swing', 'offhand')` + +Makes the living entity swing their required limb. + +### `modify(e, 'silent', boolean)` + +Silences or unsilences the entity. + +### `modify(e, 'gravity', boolean)` + +Toggles gravity for the entity. + +### `modify(e, 'invulnerable', boolean)` + +Toggles invulnerability for the entity. + +### `modify(e, 'fire', ticks)` + +Will set entity on fire for `ticks` ticks. Set to 0 to extinguish. + +### `modify(e, 'frost', ticks)` + +Will give entity frost for `ticks` ticks. Set to 0 to unfreeze. + +### `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, 'absorption', value)` + +Sets the absorption value for the player. Each point is half a yellow heart. + +### `modify(e, 'add_xp', value)` +### `modify(e, 'xp_level', value)` +### `modify(e, 'xp_progress', value)` +### `modify(e, 'xp_score', value)` + +Manipulates player xp values - `'add_xp'` the method you probably want to use +to manipulate how much 'xp' an action should give. `'xp_score'` only affects the number you see when you die, and +`'xp_progress'` controls the xp progressbar above the hotbar, should take values from 0 to 1, but you can set it to any value, +maybe you will get a double, who knows. + +### `modify(e, 'air', ticks)` + +Modifies entity air. + +### `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' an action costs. + +### `modify(e, 'breaking_progress', value)` + +Modifies the breaking progress of a player currently mined block. Value of `null`, `-1` makes it reset. +Values `0` to `10` will show respective animation of a breaking block. Check `query(e, 'breaking_progress')` for +examples. + +### `modify(e, 'nbt_merge', partial_tag)` + +Merges a partial tag into the entity data and reloads the entity from its updated tag. Cannot be applied to players. + +### `modify(e, 'nbt', tag)` + +Reloads the entity from a supplied tag. Better use a valid entity tag, what can go wrong? Wonder what would happen if you +transplant rabbit's brain into a villager? Cannot be applied to players. + +## 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. +The event handler is any function that runs in your package that accepts certain expected parameters, which you can +expand with your own arguments. When it comes to the moment when the given command needs to be executed, it does so +providing that number of arguments it accepts is equal number of event arguments, and extra arguments passed when +defining the callback with `entity_event`. + +The following events can be handled by entities: + +* `'on_tick'`: executes every tick right before the entity is ticked in the game. Required arguments: `entity` +* `'on_move'`: executes every time an entity changes position, invoked just after it has been moved to the new position. Required arguments: `entity, velocity, pos1, pos2` +* `'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 attach an inapplicable event to it. + +In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + +### `entity_load_handler(descriptor / descriptors, function)`, `entity_load_handler(descriptor / descriptors, call_name, ... args?)` + +Attaches a callback to trigger when any entity matching the following type / types is loaded in the game, allowing to grab a handle +to an entity right when it is loaded to the world without querying them every tick. Callback expects two parameters - the entity, +and a boolean value indicating if the entity was newly created(`true`) or just loaded from disk. Single argument functions accepting +only entities are allowed, but deprecated and will be removed at some point. + +If callback is `null`, then the current entity handler, if present, is removed. Consecutive calls to `entity_load_handler` will add / subtract +of the currently targeted entity types pool. + +Like other global events, calls to `entity_load_handler` should only be attached in apps with global scope. For player scope apps, +it will be called multiple times, once for each player. That's likely not what you want to do. + +``` +// veryfast method of getting rid of all the zombies. Callback is so early, its packets haven't reached yet the clients +// so to save on log errors, removal of mobs needs to be scheduled for later. +entity_load_handler('zombie', _(e, new) -> schedule(0, _(outer(e)) -> modify(e, 'remove'))) + +// another way to do it is to remove the entity when it starts ticking +entity_load_handler('zombie', _(e, new) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'remove'))) + +// making all zombies immediately faster and less susceptible to friction of any sort +entity_load_handler('zombie', _(e, new) -> entity_event(e, 'on_tick', _(e) -> modify(e, 'motion', 1.2*e~'motion'))) +``` + +Word of caution: entities can be loaded with chunks in various states, for instance when a chunk is being generated, this means +that accessing world blocks would cause the game to freeze due to force generating that chunk while generating the chunk. Make +sure to never assume the chunk is ready and use `entity_load_handler` to schedule actions around the loaded entity, +or manipulate entity directly. + +Also, it is possible that mobs that spawn with world generation, while being 'added' have their metadata serialized and cached +internally (vanilla limitation), so some modifications to these entities may have no effect on them. This affects mobs created with +world generation. + +For instance the following handler is safe, as it only accesses the entity directly. It makes all spawned pigmen jump +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, modify(e, 'motion', 0, 1, 0)) ) +``` +But the following handler, attempting to despawn pigmen that spawn in portals, will cause the game to freeze due to cascading access to blocks that would cause neighbouring chunks +to force generate, causing also error messages for all pigmen caused by packets send after entity is removed by script. +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new && block(pos(e))=='nether_portal', modify(e, 'remove') ) ) +``` +Easiest method to circumvent these issues is delay the check, which may or may not cause cascade load to happen, but +will definitely break the infinite chain. +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, schedule(0, _(outer(e)) -> if(block(pos(e))=='nether_portal', modify(e, 'remove') ) ) ) ) +``` +But the best is to perform the check first time the entity will be ticked - giving the game all the time to ensure chunk +is fully loaded and entity processing, removing the tick handler: +``` +/script run entity_load_handler('zombified_piglin', _(e, new) -> if(new, entity_event(e, 'on_tick', _(e) -> ( if(block(pos(e))=='nether_portal', modify(e, 'remove')); entity_event(e, 'on_tick', null) ) ) ) ) +``` +Looks little convoluted, but that's the safest method to ensure your app won't crash. + +### `entity_event(e, event, function)`, `entity_event(e, event, call_name, ... args?)` + +Attaches specific function from the current package to be called upon the `event`, with extra `args` carried to the +original required arguments for the event handler. + +
+protect_villager(entity, amount, source, source_entity, healing_player) ->
+(
+   if(source_entity && source_entity~'type' != 'player',
+      modify(entity, 'health', amount + entity~'health' );
+      particle('end_rod', pos(entity)+[0,3,0]);
+      print(str('%s healed thanks to %s', entity, healing_player))
+   )
+);
+__on_player_interacts_with_entity(player, entity, hand) ->
+(
+   if (entity~'type' == 'villager',
+      entity_event(entity, 'on_damage', 'protect_villager', player~'name')
+   )
+)
+
+ +In this case this will protect a villager from entity damage (zombies, etc.) except from players by granting all the +health back to the villager after being harmed. diff --git a/docs/scarpet/api/Events.md b/docs/scarpet/api/Events.md new file mode 100644 index 0000000..59378f9 --- /dev/null +++ b/docs/scarpet/api/Events.md @@ -0,0 +1,452 @@ +# Scarpet events system + +Scarpet provides the ability to execute specific function whenever an event occurs. The functions to be subscribed for an event +need to conform with the arguments to the event specification. There are several built-in events triggered when certain in-game +events occur, but app designers can create their own events and trigger them across all loaded apps. + +When loading the app, each function that starts +with `__on_` and has the required arguments, will be bound automatically to a corresponding built-in event. '`undef`'ying +of such function would result in unbinding the app from this event. Defining event hanlder via `__on_(... args) -> expr` is +equivalent of defining it via `handle_event('', _(... args) -> expr)` + +In case of `player` scoped apps, +all player action events will be directed to the appropriate player hosts. Global events, like `'tick'`, that don't have a specific +player target will be executed multiple times, once for each player app instance. While each player app instance is independent, +statically defined event handlers will be copied to each players app, but if you want to apply them in more controlled way, +defining event handlers for each player in `__on_start()` function is preferred. + +Most built-in events strive to report right before they take an effect in the game. The purpose of that is that this give a choice +for the programmer to handle them right away (as it happens, potentially affect the course of action by changing the +environment right before it), or decide to handle it after by scheduling another call for the end of the tick. Or both - +partially handle the event before it happens and handle the rest after. While in some cases this may lead to programmers +confusion (like handling the respawn event still referring to player's original position and dimension), but gives much +more control over these events. + +Some events also provide the ability to cancel minecraft's processing of the event by returning `'cancel'` from the event handler. +This only works for particular events that are triggered before they take an effect in the game. +However, cancelling the event will also stop events from subsequent apps from triggering. +The order of events being executed can be changed by specifying an `'event_priority'` in the app config, +with the highest value being executed first. +Note that cancelling some events might introduce a desynchronization to the client from the server, +creating ghost items or blocks. This can be solved by updating the inventory or block to the client, by using `inventory_set` or `set`. + +Programmers can also define their own events and signal other events, including built-in events, and across all loaded apps. + +## App scopes and event distribution + +Events triggered in an app can result in zero, one, or multiple executions, depending on the type of the event, and the app scope. + * player targeted events (like `player_breaks_block`) target each app once: + * for global scoped apps - targets a single app instance and provides `player` as the first argument. + * for player scoped apps - targets only a given player instance, providing player argument for API consistency, + since active player in player scoped apps can always be retrieved using `player()`. + * global events could be handled by multiple players multiple times (like `explosion`, or `tick`): + * for global scoped apps - triggered once for the single app instance. + * for player scoped apps - triggered N times for each player separately, so they can do something with that information + * custom player targeted events (using `signal_event(, , data)`): + * for global scoped apps - doesn't trigger at all, since there is no way to pass the required player. + To target global apps with player information, use `null` for player target, and add player information to the `data` + * for player scoped apps - triggers once for the specified player and its app instance + * custom general events (using `signal_event(, null, data)`) behave same like built-in global events: + * for global scoped apps - triggers once for the only global instance + * for player scoped apps - triggers N times, once for each player app instance + +## Built-in events + +Here is the list of events that are handled by default in scarpet. This list includes prefixes for function names, allowing apps +to register them when the app starts, but you can always add any handler function to any event using `/script event` command, +if it accepts the required number of parameters for the event. + +## Meta-events + +These events are not controlled / triggered by the game per se, but are important for the flow of the apps, however for all +intent and purpose can be treated as regular events. Unlike regular events, they cannot be hooked up to with `handle_event()`, +and the apps themselves need to have them defined as distinct function definitions. They also cannot be triggered via `signal_event()`. + +### `__on_start()` +Called once per app in its logical execution run. For `'global'` scope apps its executed right after the app is loaded. For +`'player'` scope apps, it is triggered once per player before the app can be used by that player. Since each player app acts +independently from other player apps, this is probably the best location to include some player specific initializations. Static +code (i.e. code typed directly in the app code that executes immediately, outside of function definitions), will only execute once +per app, regardless of scope, `'__on_start()'` allows to reliably call player specific initializations. However, most event handlers +defined in the static body of the app will be copied over to each player scoped instance when they join. + +### `__on_close()` + +Called once per app when the app is closing or reloading, right before the app is removed. +For player scoped apps, its called once per player. Scarpet app engine will attempt to call `'__on_close()'` even if +the system is closing down exceptionally. + + +## Built-in global events + +Global events will be handled once per app that is with `'global'` scope. With `player` scoped apps, each player instance + will be triggered once for each player, so a global event may be executed multiple times for such apps. + +### `__on_server_starts()` +Event triggers after world is loaded and after all startup apps have started. It won't be triggered with `/reload`. + +### `__on_server_shuts_down()` +Event triggers when the server started the shutdown process, before `__on_close()` is executed. Unlike `__on_close()`, it doesn't +trigger with `/reload`. + +### `__on_tick()` +Event triggers at the beginning of each tick, located in the overworld. You can use `in_dimension()` +to access other dimensions from there. + +### `__on_tick_nether()` (Deprecated) +Duplicate of `tick`, just automatically located in the nether. Use `__on_tick() -> in_dimension('nether', ... ` instead. + +### `__on_tick_ender()` (Deprecated) +Duplicate of `tick`, just automatically located in the end. Use `__on_tick() -> in_dimension('end', ... ` instead. + +### `__on_chunk_generated(x, z)` +Called right after a chunk at a given coordinate is full generated. `x` and `z` correspond +to the lowest x and z coords in the chunk. Handling of this event is scheduled as an off-tick task happening after the +chunk is confirmed to be generated and loaded to the game, due to the off-thread chunk loading in the game. So +handling of this event is not technically guaranteed if the game crashes while players are moving for example, and the game +decides to shut down after chunk is fully loaded and before its handler is processed in between ticks. In normal operation +this should not happen, but let you be warned. + +### `__on_chunk_loaded(x, z)` +Called right after a chunk at a given coordinate is loaded. All newly generated chunks are considered loaded as well. + `x` and `z` correspond to the lowest x and z coordinates in the chunk. + +### `__on_chunk_unloaded(x, z)` +Called right before a chunk at the given coordinates is unloaded. `x` and `z` correspond to the lowest x and z coordinates in the chunk. + +### `__on_lightning(block, mode)` +Triggered right after a lightning strikes. Lightning entity as well as potential horseman trap would +already be spawned at that point. `mode` is `true` if the lightning did cause a trap to spawn. + +### `__on_explosion(pos, power, source, causer, mode, fire)` + +Event triggered right before explosion takes place and before has any effect on the world. `source` can be an entity causing +the explosion, and `causer` the entity triggering it, +`mode` indicates block effects: `'none'`, `'break'` (drop all blocks), or `'destroy'` - drop few blocks. Event +is not captured when `create_explosion()` is called. + +### `__on_explosion_outcome(pos, power, source, causer, mode, fire, blocks, entities)` +Triggered during the explosion, before any changes to the blocks are done, +but the decision to blow up is already made and entities are already affected. +The parameter `blocks` contains the list of blocks that will blow up (empty if `explosionNoBlockDamage` is set to `true`). +The parameter `entities` contains the list of entities that have been affected by the explosion. Triggered even with `create_explosion()`. + +### `__on_carpet_rule_changes(rule, new_value)` +Triggered when a carpet mod rule is changed. It includes extension rules, not using default `/carpet` command, +which will then be namespaced as `namespace:rule`. + +### Entity load event -> check in details on `entity_load_handler()` + +These will trigger every time an entity of a given type is loaded into the game: spawned, added with a chunks, +spawned from commands, anything really. Check `entity_load_handler()` in the entity section for details. + +## Built-in player events + +These are triggered with a player context. For apps with a `'player'` scope, they trigger once for the appropriate +player. In apps with `global` scope they trigger once as well as a global event. + +### `__on_player_uses_item(player, item_tuple, hand)` +Triggers with a right click action. Event is triggered right after a server receives the packet, before the +game manages to do anything about it. Event triggers when player starts eating food, or starts drawing a bow. +Use `player_finishes_using_item`, or `player_releases_item` to capture the end of these events. + +This event can be cancelled by returning `'cancel'`, which prevents the item from being used. + +Event is not triggered when a player places a block, for that use +`player_right_clicks_block` or `player_places_block` event. + +### `__on_player_releases_item(player, item_tuple, hand)` +Player stops right-click-holding on an item that can be held. This event is a result of a client request. +Example events that may cause it to happen is releasing a bow. The event is triggered after the game processes +the request, however the `item_tuple` is provided representing the item that the player started with. You can use that and +compare with the currently held item for a delta. + +### `__on_player_finishes_using_item(player, item_tuple, hand)` +Player using of an item is done. This is controlled server side and is responsible for such events as finishing +eating. The event is triggered after confirming that the action is valid, and sending the feedback back +to the client, but before triggering it and its effects in game. + +This event can be cancelled by returning `'cancel'`, which prevents the player from finishing using the item. + +### `__on_player_clicks_block(player, block, face)` +Representing left-click attack on a block, usually signifying start of breaking of a block. Triggers right after the server +receives a client packet, before anything happens on the server side. + +This event can be cancelled by returning `'cancel'`, which stops the player from breaking a block. + + +### `__on_player_breaks_block(player, block)` +Called when player breaks a block, right before any changes to the world are done, but the decision is made to remove the block. + +This event can be cancelled by returning `'cancel'`, which prevents the block from being placed. + +### `__on_player_right_clicks_block(player, item_tuple, hand, block, face, hitvec)` +Called when player right clicks on a block with anything, or interacts with a block. This event is triggered right +before other interaction events, like `'player_interacts_with_block'` or `'player_places_block'`. + +This event can be cancelled by returning `'cancel'`, which prevents the player interaction. + +### `__on_player_interacts_with_block(player, hand, block, face, hitvec)` +Called when player successfully interacted with a block, which resulted in activation of said block, +right after this happened. + +### `__on_player_placing_block(player, item_tuple, hand, block)` +Triggered when player places a block, before block is placed in the world. + +This event can be cancelled by returning `'cancel'`, which prevents the block from being placed. + +### `__on_player_places_block(player, item_tuple, hand, block)` +Triggered when player places a block, after block is placed in the world, but before scoreboard is triggered or player inventory +adjusted. + +### `__on_player_interacts_with_entity(player, entity, hand)` +Triggered when player right clicks (interacts) with an entity, even if the entity has no vanilla interaction with the player or +the item they are holding. The event is invoked after receiving a packet from the client, before anything happens server side +with that interaction. + +This event can be cancelled by returning `'cancel'`, which prevents the player interacting with the entity. + +### `__on_player_trades(player, entity, buy_left, buy_right, sell)` +Triggered when player trades with a merchant. The event is invoked after the server allow the trade, but before the inventory +changes and merchant updates its trade-uses counter. +The parameter `entity` can be `null` if the merchant is not an entity. + +### `__on_player_collides_with_entity(player, entity)` +Triggered every time a player - entity collisions are calculated, before effects of collisions are applied in the game. +Useful not only when colliding with living entities, but also to intercept items or XP orbs before they have an effect +on the player. + +### `__on_player_chooses_recipe(player, recipe, full_stack)` +Triggered when a player clicks a recipe in the crafting window from the crafting book, after server received +a client request, but before any items are moved from its inventory to the crafting menu. + +This event can be cancelled by returning `'cancel'`, which prevents the recipe from being moved into the crafting grid. + +### `__on_player_switches_slot(player, from, to)` +Triggered when a player changes their selected hotbar slot. Applied right after the server receives the message to switch +the slot. + +### `__on_player_swaps_hands(player)` +Triggered when a player sends a command to swap their offhand item. Executed before the effect is applied on the server. + +This event can be cancelled by returning `'cancel'`, which prevents the hands from being swapped. + +### `__on_player_swings_hand(player, hand)` +Triggered when a player starts swinging their hand. The event typically triggers after a corresponding event that caused it +(`player_uses_item`, `player_breaks_block`, etc.), but it triggers also after some failed events, like attacking the air. When +swinging continues as an effect of an action, no new swinging events will be issued until the swinging is stopped. + +### `__on_player_attacks_entity(player, entity)` +Triggered when a player attacks entity, right before it happens server side. + +This event can be cancelled by returning `'cancel'`, which prevents the player from attacking the entity. + +### `__on_player_takes_damage(player, amount, source, source_entity)` +Triggered when a player is taking damage. Event is executed right after potential absorption was applied and before +the actual damage is applied to the player. + +This event can be cancelled by returning `'cancel'`, which prevents the player from taking damage. + +### `__on_player_deals_damage(player, amount, entity)` +Triggered when a player deals damage to another entity. Its applied in the same moment as `player_takes_damage` if both +sides of the event are players, and similar for all other entities, just their absorption is taken twice, just noone ever +notices that ¯\_(ツ)_/¯ + +This event can be cancelled by returning `'cancel'`, which prevents the damage from being dealt. + +### `__on_player_dies(player)` +Triggered when a player dies. Player is already dead, so don't revive them then. Event applied before broadcasting messages +about players death and applying external effects (like mob anger etc). + +### `__on_player_respawns(player)` +Triggered when a player respawns. This includes spawning after death, or landing in the overworld after leaving the end. +When the event is handled, a player is still in its previous location and dimension - will be repositioned right after. In +case player died, its previous inventory as already been scattered, and its current inventory will not be copied to the respawned +entity, so any manipulation to player data is +best to be scheduled at the end of the tick, but you can still use its current reference to query its status as of the respawn event. + +### `__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension)` +Called when a player moves from one dimension to another. Event is handled still when the player is in its previous +dimension and position. + +`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 controlled by the teleport, or a player can still see the end credits. After + the player is eligible to respawn in the overworld, `player_respawns` will be triggered. + +### `__on_player_rides(player, forward, strafe, jumping, sneaking)` +Triggers when a server receives movement controls when riding vehicles. Its handled before the effects are applied +server side. + +### `__on_player_jumps(player)` +Triggered when a game receives a jump input from the client, and the player is considered standing on the ground. + + +### `__on_player_deploys_elytra(player)` +Triggered when a server receives a request to deploy elytra, regardless if the flight was agreed upon server side.. + +### `__on_player_wakes_up(player)` +Player wakes up from the bed mid sleep, but not when it is kicked out of bed because it finished sleeping. + +### `__on_player_escapes_sleep(player)` +Same as `player_wakes_up` but only triggered when pressing the ESC button. Not sure why Mojang decided to send that event +twice when pressing escape, but might be interesting to be able to detect that. + +### `__on_player_starts_sneaking(player)` +### `__on_player_stops_sneaking(player)` +### `__on_player_starts_sprinting(player)` +### `__on_player_stops_sprinting(player)` +Four events triggered when player controls for sneaking and sprinting toggle. + +### `__on_player_drops_item(player)` +### `__on_player_drops_stack(player)` +Triggered when the game receives the request from a player to drop one item or full stack from its inventory. +Event happens before anything is changed server side. + +These events can be cancelled by returning `'cancel'`, which prevents the player dropping the items. + +### `__on_player_picks_up_item(player, item)` +Triggered AFTER a player successfully ingested an item in its inventory. Item represents the total stack of items +ingested by the player. The exact position of these items is unknown as technically these +items could be spread all across the inventory. + +### `__on_player_connects(player)` +Triggered when the player has successfully logged in and was placed in the game. + +### `__on_player_disconnects(player, reason)` +Triggered when a player sends a disconnect package or is forcefully disconnected from the server. + +### `__on_player_message(player, message)` +Triggered when a player sends a chat message. + +### `__on_player_command(player, command)` +Triggered when a player runs a command. Command value is returned without the / in front. + +This event can be cancelled by returning `'cancel'`, which prevents the message from being sent. + +### `__on_statistic(player, category, event, value)` +Triggered when a player statistic changes. Doesn't notify on periodic an rhythmic events, i.e. +`time_since_death`, `time_since_rest`, and `played_one_minute` since these are triggered every tick. Event +is handled before scoreboard values for these statistics are changed. + +## Custom events and hacking into scarpet event system + +App programmers can define and trigger their own custom events. Unlike built-in events, all custom events pass a single value +as an argument, but this doesn't mean that they cannot pass a complex list, map, or nbt tag as a message. Each event signal is +either targeting all apps instances for all players, including global apps, if no target player has been identified, +or only player scoped apps, if the target player +is specified, running once for that player app. You cannot target global apps with player-targeted signals. Built-in events +do target global apps, since their first argument is clearly defined and passed. That may change in the future in case there is +a compelling argument to be able to target global apps with player scopes. + +Programmers can also handle built-in events the same way as custom events, as well as triggering built-in events, which I have +have no idea why you would need that. The following snippets have the same effect: + +``` +__on_player_breaks_block(player, block) -> print(player+' broke '+block); +``` +and +``` +handle_event('player_breaks_block', _(player, block) -> print(player+' broke '+block)); +``` + +as well as +``` +undef('__on_player_breaks_block'); +``` +and +``` +handle_event('player_breaks_block', null); +``` +And `signal_event` can be used as a trigger, called twice for player based built-in events +``` +signal_event('player_breaks_block', player, player, block); // to target all player scoped apps +signal_event('player_breaks_block', null , player, block); // to target all global scoped apps and all player instances +``` +or (for global events) +``` +signal_event('tick') // trigger all apps with a tick event +``` + +### `handle_event(event, callback ...)` + +Provides a handler for an event identified by the '`event`' argument. If the event doesn't exist yet, it will be created. +All loaded apps globally can trigger that event, when they call corresponding `signal_event(event, ...)`. Callback can be +defined as a function name, function value (or a lambda function), along with optional extra arguments that will be passed +to it when the event is triggered. All custom events expect a function that takes one free argument, passed by the event trigger. +If extra arguments are provided, they will be appended to the argument list of the callback function. + +Returns `true` if subscription to the event was successful, or `false` if it failed (for instance wrong scope for built-in event, +or incorrect number of parameters for the event). + +If a callback is specified as `null`, the given app (or player app instance )stops handling that event. + +
+foo(a) -> print(a);
+handle_event('boohoo', 'foo');
+
+bar(a, b, c) -> print([a, b, c]);
+handle_event('boohoo', 'bar', 2, 3) // using b = 2, c = 3, a - passed by the caller
+
+handle_event('tick', _() -> foo('tick happened')); // built-in event
+
+handle_event('tick', null)  // nah, ima good, kthxbai
+
+ +In case you want to pass an event handler that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + + +### `signal_event(event, target_player?, ... args?)` + +Fires a specific event. If the event does not exist (only `handle_event` creates missing new events), or provided argument list +was not matching the callee expected arguments, returns `null`, +otherwise returns number of apps notified. If `target_player` is specified and not `null` triggers a player specific event, targeting +only `player` scoped apps for that player. Apps with globals scope will not be notified even if they handle this event. +If the `target_player` is omitted or `null`, it will target `global` scoped apps and all instances of `player` scoped apps. +Note that all built-in player events have a player as a first argument, so to trigger these events, you need to +provide them twice - once to specify the target player scope and second - to provide as an argument to the handler function. + +
+signal_event('player_breaks_block', player, player, block); // to target all player scoped apps
+signal_event('player_breaks_block', null  , player, block); // to target all global scoped apps and all player instances
+signal_event('tick') // trigger all apps with a tick event
+
+ +## Custom events example + +The following example shows how you can communicate between different instances of the same player scoped app. It important to note +that signals can trigger other apps as well, assuming the name of the event matches. In this case the request name is called +`tp_request` and is triggered with a command. + + +``` +// tpa.sc +global_requester = null; +__config() -> { + 'commands' -> { + '' -> _(to) -> signal_event('tp_request', to, player()), + 'accept' -> _() -> if(global_requester, + run('tp '+global_requester~'command_name'); + global_requester = null + ) + }, + 'arguments' -> { + 'player' -> {'type' -> 'players', 'single' -> true} + } +}; +handle_event('tp_request', _(req) -> ( + global_requester = req; + print(player(), format( + 'w '+req+' requested to teleport to you. Click ', + 'yb here', '^yb here', '!/tpa accept', + 'w to accept it.' + )); +)); +``` + +## `/script event` command + +used to display current events and bounded functions. use `add_to` to register a new event, or `remove_from` to +unbind a specific function from an event. Function to be bounded to an event needs to have the same number of +parameters as the action is attempting to bind to (see list above). All calls in modules loaded via `/script load` +that handle specific built-in events will be automatically bounded, and unbounded when script is unloaded. diff --git a/docs/scarpet/api/Inventories.md b/docs/scarpet/api/Inventories.md new file mode 100644 index 0000000..23d0735 --- /dev/null +++ b/docs/scarpet/api/Inventories.md @@ -0,0 +1,468 @@ +# Inventory and Items API + +## Manipulating inventories of blocks and entities + +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, or can be preceded with inventory +type. +Inventory type can be `null` (default), `'enderchest'` denoting player enderchest storage, or `'equipment'` applying to +entities hand and armour pieces. Then the type can be followed by entity, block or position coordinates. +For instance, player enderchest inventory requires +two arguments, keyword `'enderchest'`, followed by the player entity argument, (or a single argument as a string of a +form: `'enderchest_steve'` for legacy support). If your player name starts with `'enderchest_'`, first of all, tough luck, +but then 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. + +A few living entities can have both: their regular inventory, and their equipment inventory. +Player's regular inventory already contains the equipment, but you can access the equipment part as well, as well as +their enderchest separately. For entity types that only have +their equipment inventory, the equipment is returned by default (`null` type). + +If that's confusing see examples under `inventory_size` on how to access inventories. All other `inventory_...()` functions +use the same scheme. + + + If the entity or a block doesn't have +an inventory, all API functions typically do nothing and return null. + +Most items returned are in the form of a triple of item name, count, and the full nbt of an item. When saving an item, if the +nbt is provided, it overrides the item type provided in the name. + +### `item_list(tag?)` + +With no arguments, returns a list of all items in the game. With an item tag provided, list items matching the tag, or `null` if tag is not valid. + +### `item_tags(item, tag?)` + +Returns list of tags the item belongs to, or, if tag is provided, `true` if an item matches the tag, `false` if it doesn't and `null` if that's not a valid tag + +Throws `unknown_item` if item doesn't exist. + +### `stack_limit(item)` + +Returns number indicating what is the stack limit for the item. Its typically 1 (non-stackable), 16 (like buckets), +or 64 - rest. It is recommended to consult this, as other inventory API functions ignore normal stack limits, and +it is up to the programmer to keep it at bay. As of 1.13, game checks for negative numbers and setting an item to +negative is the same as empty. + +Throws `unknown_item` if item doesn't exist. + +
+stack_limit('wooden_axe') => 1
+stack_limit('ender_pearl') => 16
+stack_limit('stone') => 64
+
+ +### `recipe_data(item, type?)`, `recipe_data(recipe, type?)` + +returns all recipes matching either an `item`, or represent actual `recipe` name. In vanilla datapack, for all items +that have one recipe available, the recipe name is the same as the item name but if an item has multiple recipes, its +direct name can be different. + +Recipe type can take one of the following options: + * `'crafting'` - default, crafting table recipe + * `'smelting'` - furnace recipe + * `'blasting'` - blast furnace recipe + * `'smoking'` - smoker recipe + * `'campfire_cooking'` - campfire recipe + * `'stonecutting'` - stonecutter recipe + * `'smithing'` - smithing table (1.16+) + + The return value is a list of available recipes (even if there is only one recipe available). Each recipe contains of + an item triple of the crafting result, list of ingredients, each containing a list of possible variants of the + ingredients in this slot, as item triples, or `null` if its a shaped recipe and a given slot in the patterns is left + empty, and recipe specification as another list. Possible recipe specs is: + * `['shaped', width, height]` - shaped crafting. `width` and `height` can be 1, 2 or 3. + * `['shapeless']` - shapeless crafting + * `['smelting', duration, xp]` - smelting/cooking recipes + * `['cutting']` - stonecutter recipe + * `['special']` - special crafting recipe, typically not present in the crafting menu + * `['custom']` - other recipe types + +Note that ingredients are specified as tripes, with count and nbt information. Currently all recipes require always one +of the ingredients, and for some recipes, even if the nbt data for the ingredient is specified (e.g. `dispenser`), it +can accept items of any tags. + +Also note that some recipes leave some products in the crafting window, and these can be determined using + `crafting_remaining_item()` function + + Examples: +
+ recipe_data('iron_ingot_from_nuggets')
+ recipe_data('iron_ingot')
+ recipe_data('glass', 'smelting')
+ 
+ +### `crafting_remaining_item(item)` + +returns `null` if the item has no remaining item in the crafting window when used as a crafting ingredient, or an +item name that serves as a replacement after crafting is done. Currently it can only be buckets and glass bottles. + +### `inventory_size(inventory)` + +Returns the size of the inventory for the entity or block in question. Returns null if the block or entity don't +have an inventory. + +
+inventory_size(player()) => 41
+inventory_size('enderchest', player()) => 27 // enderchest
+inventory_size('equipment', player()) => 6 // equipment
+inventory_size(null, player()) => 41  // default inventory for players
+
+inventory_size(x,y,z) => 27 // chest
+inventory_size(block(pos)) => 5 // hopper
+
+horse = spawn('horse', x, y, z);
+inventory_size(horse); => 2 // default horse inventory
+inventory_size('equipment', horse); => 6 // unused horse equipment inventory
+inventory_size(null, horse); => 2 // default horse
+
+creeper = spawn('creeper', x, y, z);
+inventory_size(creeper); => 6 // default creeper inventory is equipment since it has no other
+inventory_size('equipment', creeper); => 6 // unused horse equipment inventory
+inventory_size(null, creeper); => 6 // creeper default is its equipment
+
+ +### `inventory_has_items(inventory)` + +Returns true, if the inventory is not empty, false if it is empty, and null, if its not an inventory. + +
    inventory_has_items(player()) => true
+    inventory_has_items(x,y,z) => false // empty chest
+    inventory_has_items(block(pos)) => null // stone
+
+ +### `inventory_get(inventory, slot)` + +Returns the item in the corresponding inventory slot, or null if slot empty or inventory is invalid. You can use +negative numbers to indicate slots counted from 'the back'. + +
+inventory_get(player(), 0) => null // nothing in first hotbar slot
+inventory_get(x,y,z, 5) => ['stone', 1, {id:"minecraft:stone"}]
+inventory_get(player(), -1) => ['diamond_pickaxe', 1, {components:{"minecraft:damage":4},id:"minecraft:diamond_pickaxe"}] // slightly damaged diamond pick in the offhand
+
+ +### `inventory_set(inventory, slot, count, item?, nbt?)` + +Modifies or sets a stack in inventory. specify count 0 to empty the slot. If item is not specified, keeps existing +item, just modifies the count. If item is provided - replaces current item. If nbt is provided - uses the tag to create the item fully +ignoring the item name. If nbt is provided and count is not null, the sets the custom count on the tag from the count parameter. +If count is `null` and item is `null`, an item is entirely defined by the `nbt` parameter. Returns previous stack in that slot. + +
+inventory_set(player(), 0, 0) => ['stone', 64, {id:"minecraft:stone"}] // player had a stack of stone in first hotbar slot
+inventory_set(player(), 0, 6) => ['diamond', 64, {id:"minecraft:diamond"}] // changed stack of diamonds in player slot to 6
+inventory_set(player(), 0, 1, 'diamond_axe','{components:{"minecraft:damage":5},id:"minecraft:diamond_axe"}') => null //added slightly damaged diamond axe to first player slot
+inventory_set(player(), 0, null, null, '{components:{"minecraft:damage":5},id:"minecraft:diamond_axe"}') => null // same effect as above
+
+ +### `inventory_find(inventory, item, start_slot?, ), inventory_find(inventory, null, start_slot?)` + +Finds the first slot with a corresponding item in the inventory, or if queried with null: the first empty slot. +Returns slot number if found, or null otherwise. Optional start_slot argument allows to skip all preceeding slots +allowing for efficient (so not slot-by-slot) inventory search for items. + +
+inventory_find(player(), 'stone') => 0 // player has stone in first hotbar slot
+inventory_find(player(), null) => null // player's inventory has no empty spot
+while( (slot = inventory_find(p, 'diamond', slot)) != null, 41, drop_item(p, slot) )
+    // spits all diamonds from player inventory wherever they are
+inventory_drop(x,y,z, 0) => 64 // removed and spawned in the world a full stack of items
+
+ +Throws `unknown_item` if item doesn't exist. + +### `inventory_remove(inventory, item, amount?)` + +Removes amount (defaults to 1) of item from inventory. If the inventory doesn't have the defined amount, nothing +happens, otherwise the given amount of items is removed wherever they are in the inventory. Returns boolean +whether the removal operation was successful. Easiest way to remove a specific item from player inventory +without specifying the slot. + +
+inventory_remove(player(), 'diamond') => 1 // removed diamond from player inventory
+inventory_remove(player(), 'diamond', 100) => 0 // player doesn't have 100 diamonds, nothing happened
+
+ +### `drop_item(inventory, slot, amount?, )` + +Drops the items from indicated inventory slot, like player that Q's an item or villager, that exchanges food. +You can Q items from block inventories as well. default amount is 0 - which is all from the slot. +NOTE: hoppers are quick enough to pick all the queued items from their inventory anyways. +Returns size of the actual dropped items. + +
+inventory_drop(player(), 0, 1) => 1 // Q's one item on the ground
+inventory_drop(x,y,z, 0) => 64 // removed and spawned in the world a full stack of items
+
+ +## Screens + +A screen is a value type used to open screens for a player and interact with them. +For example, this includes the chest inventory gui, the crafting table gui and many more. + +### `create_screen(player, type, name, callback?)` + +Creates and opens a screen for a `player`. + +Available `type`s: + +* `anvil` +* `beacon` +* `blast_furnace` +* `brewing_stand` +* `cartography_table` +* `crafting` +* `enchantment` +* `furnace` +* `generic_3x3` +* `generic_9x1` +* `generic_9x2` +* `generic_9x3` +* `generic_9x4` +* `generic_9x5` +* `generic_9x6` +* `grindstone` +* `hopper` +* `lectern` +* `loom` +* `merchant` +* `shulker_box` +* `smithing` +* `smoker` +* `stonecutter` + +The `name` parameter can be a formatted text and will be displayed at the top of the screen. +Some screens like the lectern or beacon screen don't show it. + +Optionally, a `callback` function can be passed as the fourth argument. +This functions needs to have four parameters: +`_(screen, player, action, data) -> ...` + +The `screen` parameter is the screen value of the screen itself. +`player` is the player who interacted with the screen. +`action` is a string corresponding to the interaction type. +Can be any of the following: + +Slot interactions: + +* `pickup` +* `quick_move` +* `swap` +* `clone` +* `throw` +* `quick_craft` +* `pickup_all` + +The `data` for this interaction is a map, with a `slot` and `button` value. +`slot` is the slot index of the slot that was clicked. +When holding an item in the cursor stack and clicking inside the screen, +but not in a slot, this is -1. +If clicked outside the screen (where it would drop held items), this value is null. +The `button` is the mouse button used to click the slot. + +For the `swap` action, the `button` is the number key 0-8 for a certain hotbar slot. + +For the `quick_craft` action, the `data` also contains the `quick_craft_stage`, +which is either 0 (beginning of quick crafting), 1 (adding item to slot) or 2 (end of quick crafting). + +Other interactions: + +* `button` Pressing a button in certain screens that have button elements (enchantment table, lectern, loom and stonecutter) +The `data` provides a `button`, which is the index of the button that was pressed. +Note that for lecterns, this index can be certain a value above 100, for jumping to a certain page. +This can come from formatted text inside the book, with a `change_page` click event action. + +* `close` Triggers when the screen gets closed. No `data` provided. + +* `select_recipe` When clicking on a recipe in the recipe book. +`data` contains a `recipe`, which is the identifier of the clicked recipe, +as well as `craft_all`, which is a boolean specifying whether +shift was pressed when selecting the recipe. + +* `slot_update` Gets called **after** a slot has changed contents. `data` provides a `slot` and `stack`. + +By returning a string `'cancel'` in the callback function, +the screen interaction can be cancelled. +This doesn't work for the `close` action. + +The `create_screen` function returns a `screen` value, +which can be used in all inventory related functions to access the screens' slots. +The screen inventory covers all slots in the screen and the player inventory. +The last slot is the cursor stack of the screen, +meaning that using `-1` can be used to modify the stack the players' cursor is holding. + +### `close_screen(screen)` + +Closes the screen of the given screen value. +Returns `true` if the screen was closed. +If the screen is already closed, returns `false`. + +### `screen_property(screen, property)` + +### `screen_property(screen, property, value)` + +Queries or modifies a certain `property` of a `screen`. +The `property` is a string with the name of the property. +When called with `screen` and `property` parameter, returns the current value of the property. +When specifying a `value`, +the property will be assigned the new `value` and synced with the client. + +**Options for `property` string:** + +| `property` | Required screen type | Type | Description | +|---|---|---|---| +| `name` | **All** | text | The name of the screen, as specified in the `create_screen()` function. Can only be queried. | +| `open` | **All** | boolean | Returns `true` if the screen is open, `false` otherwise. Can only be queried. | +| `fuel_progress` | furnace/smoker/blast_furnace | number | Current value of the fuel indicator. | +| `max_fuel_progress` | furnace/smoker/blast_furnace | number | Maximum value for the full fuel indicator. | +| `cook_progress` | furnace/smoker/blast_furnace | number | Cooking progress indicator value. | +| `max_cook_progress` | furnace/smoker/blast_furnace | number | Maximum value for the cooking progress indicator. | +| `level_cost` | anvil | number | Displayed level cost for the anvil. | +| `page` | lectern | number | Opened page in the lectern screen. | +| `beacon_level` | beacon | number | The power level of the beacon screen. This affects how many effects under primary power are grayed out. Should be a value between 0-5. | +| `primary_effect` | beacon | number | The effect id of the primary effect. This changes the effect icon on the button on the secondary power side next to the regeneration effect. | +| `secondary_effect` | beacon | number | The effect id of the secondary effect. This seems to change nothing, but it exists. | +| `brew_time` | brewing_stand | number | The brewing time indicator value. This goes from 0 to 400. | +| `brewing_fuel` | brewing_stand | number | The fuel indicator progress. Values range between 0 to 20. | +| `enchantment_power_x` | enchantment | number | The level cost of the shown enchantment. Replace `x` with 1, 2 or 3 (e.g. `enchantment_power_2`) to target the first, second or third enchantment. | +| `enchantment_id_x` | enchantment | number | The id of the enchantment shown (replace `x` with the enchantment slot 1/2/3). | +| `enchantment_level_x` | enchantment | number | The enchantment level of the enchantment. | +| `enchantment_seed` | enchantment | number | The seed of the enchanting screen. This affects the text shown in the standard Galactic alphabet. | +| `banner_pattern` | loom | number | The selected banner pattern inside the loom. | +| `stonecutter_recipe` | stonecutter | number | The selected recipe in the stonecutter. | + +### Screen example scripts + +
+Chest click event + +```py +__command() -> ( + create_screen(player(),'generic_9x6',format('db Test'),_(screen, player, action, data) -> ( + print(player('all'),str('%s\n%s\n%s',player,action,data)); //for testing + if(action=='pickup', + inventory_set(screen,data:'slot',1,if(inventory_get(screen,data:'slot'),'air','red_stained_glass_pane')); + ); + 'cancel' + )); +); +``` +
+ +
+Anvil text prompt + +```py +// anvil text prompt gui +__command() -> ( + global_screen = create_screen(player(),'anvil',format('r Enter a text'),_(screen, player, action, data)->( + if(action == 'pickup' && data:'slot' == 2, + renamed_item = inventory_get(screen,2); + nbt = renamed_item:2; + name = parse_nbt(nbt:'display':'Name'):'text'; + if(!name, return('cancel')); //don't accept empty string + print(player,'Text: ' + name); + close_screen(screen); + ); + 'cancel' + )); + inventory_set(global_screen,0,1,'paper','{display:{Name:\'{"text":""}\'}}'); +); + +``` +
+ +
+Lectern flip book + +```py +// flip book lectern + +global_fac = 256/60; +curve(v) -> ( + v = v%360; + if(v<60,v*global_fac,v<180,255,v<240,255-(v-180)*global_fac,0); +); + +hex_from_hue(hue) -> str('#%02X%02X%02X',curve(hue+120),curve(hue),curve(hue+240)); + +make_char(hue) -> str('{"text":"▉","color":"%s"}',hex_from_hue(hue)); + +make_page(hue) -> ( + page = '['; + loop(15, //row + y = _; + loop(14, //col + x = _; + page += make_char(hue+x*4+y*4) + ','; + ); + ); + return(slice(page,0,-2)+']'); +); + + +__command() -> ( + screen = create_screen(player(),'lectern','Lectern example (this text is not visible)',_(screen, player, action, data)->( + if(action=='button', + print(player,'Button: ' + data:'button'); + ); + 'cancel' + )); + + page_count = 60; + pages = []; + + loop(page_count, + hue = _/page_count*360; + pages += make_page(hue); + ); + + nbt = encode_nbt({ + 'pages'-> pages, + 'author'->'-', + 'title'->'-', + 'resolved'->1 + }); + + inventory_set(screen,0,1,'written_book',nbt); + + task(_(outer(screen),outer(page_count))->( + while(screen != null && screen_property(screen,'open'),100000, + p = (p+1)%page_count; + screen_property(screen,'page',p); + sleep(50); + ); + )); +); + +``` +
+ +
+generic_3x3 cursor stack + +```py +__command() -> ( + screen = create_screen(player(),'generic_3x3','Title',_(screen, player, action, data) -> ( + if(action=='pickup', + // set slot to the cursor stack item + inventory_set(screen,data:'slot',1,inventory_get(screen,-1):0); + ); + 'cancel' + )); + + task(_(outer(screen))->( + // keep the cursor stack item blinking + while(screen_property(screen,'open'),100000, + inventory_set(screen,-1,1,'red_concrete'); + sleep(500); + inventory_set(screen,-1,1,'lime_concrete'); + sleep(500); + ); + )); +); +``` +
\ No newline at end of file diff --git a/docs/scarpet/api/Overview.md b/docs/scarpet/api/Overview.md new file mode 100644 index 0000000..890246a --- /dev/null +++ b/docs/scarpet/api/Overview.md @@ -0,0 +1,385 @@ +# Minecraft specific API and `scarpet` language add-ons and commands + +Here is the gist of the Minecraft related functions. Otherwise the scarpet could live without Minecraft. + +## Global scarpet options + +These options affect directly how scarpet functions and can be triggered via `/carpet` command. + - `commandScript`: disables `/script` command making it impossible to control apps in game. Apps will still load and run + when loaded with the world (i.e. present in the world/scripts folder) + - `scriptsAutoload`: when set to `false` will prevent apps loaded with the world to load automatically. You can still + load them on demand via `/script load` command + - `commandScriptACE`: command permission level that is used to trigger commands from scarpet scripts (regardless who triggers + the code that calls the command). Defaults to `ops`, could be customized to any level via a numerical value (0, 1, 2, 3 or 4) + - `scriptsOptimization`: when disabled, disables default app compile time optimizations. If your app behaves differently with + and without optimizations, please file a bug report on the bug tracker and disable code optimizations. + - `scriptsDebugging`: Puts detailed information about apps loading, performance and runtime in system log. + - `scriptsAppStore`: location of the app store for downloadable scarpet apps - can be configured to point to other scarpet app store. + +## App structure + +The main delivery method for scarpet programs into the game is in the form of apps in `*.sc` files located in the world `scripts` +folder, flat. In singleplayer, you can also save apps in `.minecraft/config/carpet/scripts` for them to be available in any world, +and here you can actually organize them in folders. +When loaded (via `/script load` command, etc.), the game will run the content of the app once, regardless of its scope +(more about the app scopes below), without executing of any functions, unless called directly, and with the exception of the +`__config()` function, if present, which will be executed once. Loading the app will also bind specific +events to the event system (check Events section for details). + +If an app defines `__on_start()` function, it will be executed once before running anything else. For global scoped apps, +this is just after they are loaded, and for player scoped apps, before they are used first time by a player. +Unlike static code (written directly in the body of the app code), that always run once per app, this may run multiple times if +its a player app nd multiple players are on the server. + +Unloading an app removes all of its state from the game, disables commands, removes bounded events, and +saves its global state. If more cleanup is needed, one can define `__on_close()` function which will be +executed when the module is unloaded, or server is closing or crashing. However, there is no need to do that +explicitly for the things that clean up automatically, as indicated in the previous statement. With `'global'` scoped +apps `__on_close()` will execute once per app, and with `'player'` scoped apps, will execute once per player per app. + +### App config via `__config()` function + +If an app defines `__config` method, and that method returns a map, it will be used to apply custom settings +for this app. Currently, the following options are supported: + +* `'strict'` : if `true`, any use of an uninitialized variable will result in program failure. Defaults to `false` if +not specified. With `'strict'`you have to assign an initial value to any variable before using it. It is very useful +to use this setting for app debugging and for beginner programmers. Explicit initialization is not required for your +code to work, but mistakes may result from improper assumptions about initial variable values of `null`. +* `'scope'`: default scope for global variables for the app, Default is `'player'`, which means that globals and defined +functions will be unique for each player so that apps for each player will run in isolation. This is useful in +tool-like applications, where behaviour of things is always from a player's perspective. With player scope the initial run +of the app creates is initial state: defined functions, global variables, config and event handlers, which is then copied for +each player that interacts with the app. With `'global'` scope - the state created by the initial load is the only variant of +the app state and all players interactions run in the same context, sharing defined functions, globals, config and events. +`'global'` scope is most applicable to world-focused apps, where either players are not relevant, or player data is stored +explicitly keyed with players, player names, uuids, etc. +Even for `'player'` scoped apps, you can access specific player app with with commandblocks using +`/execute as run script in run ...`. +To access global/server state for a player app, which you shouldn't do, you need to disown the command from any player, +so either use a command block, or any +arbitrary entity: `/execute as @e[type=bat,limit=1] run script in globals` for instance, however +running anything in the global scope for a `'player'` scoped app is not intended. +* `'event_priority'`: defaults to `0`. This specifies the order in which events will be run, from highest to lowest. +This is need since cancelling an event will stop executing the event in subsequent apps with lower priority. +* `'stay_loaded'`: defaults to `true`. If true, and `/carpet scriptsAutoload` is turned on, the following apps will +stay loaded after startup. Otherwise, after reading the app the first time, and fetching the config, server will drop them down. + WARNING: all apps will run once at startup anyways, so be aware that their actions that are called +statically, will be performed once anyways. Only apps present in the world's `scripts` folder will be autoloaded. +* `'legacy_command_type_support'` - if `true`, and the app defines the legacy command system via `__command()` function, +all parameters of command functions will be interpreted and used using brigadier / vanilla style argument parser and their type +will be inferred from their names, otherwise +the legacy scarpet variable parser will be used to provide arguments to commands. +* `'allow_command_conflicts'` - if custom app commands tree is defined, the app engine will check and identify +conflicts and ambiguities between different paths of execution. While ambiguous commands are allowed in brigadier, +and they tend to execute correctly, the suggestion support works really poorly in these situations and scarpet +will warn and prevent such apps from loading with an error message. If `allow_command_conflicts` is specified and +`true`, then scarpet will load all provided commands regardless. +* `'requires'` - defines either a map of mod dependencies in Fabric's mod.json style, or a function to be executed. If it's a map, it will only + allow the app to load if all of the mods specified in the map meet the version criteria. If it's a function, it will prevent the app from + loading if the function does not execute to `false`, displaying whatever is returned to the user. + + Available prefixes for the version comparison are `>=`, `<=`, `>`, `<`, `~`, `^` and `=` (default if none specified), based in the spec + at [NPM docs about SemVer ranges](https://docs.npmjs.com/cli/v6/using-npm/semver#ranges) + ``` + __config() -> { + 'requires' -> { + 'carpet' -> '>=1.4.33', // Will require Carpet with a version >= 1.4.32 + 'minecraft' -> '>=1.16', // Will require Minecraft with a version >= 1.16 + 'chat-up' -> '*' // Will require any version of the chat-up mod + } + } + ``` + ``` + __config() -> { + 'requires' -> _() -> ( + d = convert_date(unix_time()); + if(d:6 == 5 && d:2 == 13, + 'Its Friday, 13th' // Will throw this if Friday 13th, will load else since `if` function returns `null` by default + ) + } + ``` +* `'command_permission'` - indicates a custom permission to run the command. It can either be a number indicating +permission level (from 1 to 4) or a string value, one of: `'all'` (default), `'ops'` (default opped player with permission level of 2), +`'server'` - command accessible only through the server console and commandblocks, but not in chat, `'players'` - opposite +of the former, allowing only use in player chat. It can also be a function (lambda function or function value, not function name) +that takes 1 parameter, which represents the calling player, or `'null'` if the command represents a server call. +The function will prevent the command from running if it evaluates to `false`. +Please note, that Minecraft evaluates eligible commands for players when they join, or on reload/restart, so if you use a +predicate that is volatile and might change, the command might falsely do or do not indicate that it is available to the player, +however player will always be able to type it in and either succeed, or fail, based on their current permissions. +Custom permission applies to legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below. +* `'resources'` - list of all downloadable resources when installing the app from an app store. List of resources needs to be +in a list and contain of map-like resources descriptors, looking like + ``` + 'resources' -> [ + { + 'source' -> 'https://raw.githubusercontent.com/gnembon/fabric-carpet/master/src/main/resources/assets/carpet/icon.png', + 'target' -> 'foo/photos.zip/foo/cm.png', + }, + { + 'source' -> '/survival/README.md', + 'target' -> 'survival_readme.md', + 'shared' -> true, + }, + { + 'source' -> 'circle.sc', // Relative path + 'target' -> 'apps/circle.sc', // This won't install the app, use 'libraries' for that + }, + ] + ``` + `source` indicates resource location: either an arbitrary url (starting with `http://` or `https://`), + absolute location of a file in the app store (starting with a slash `/`), +or a relative location in the same folder as the app in question (the relative location directly). +`'target'` points to the path in app data, or shared app data folder. If not specified it will place the app into the main data folder with the name it has. +if `'shared'` is specified and `true`. When re-downloading the app, all resources will be re-downloaded as well. +Currently, app resources are only downloaded when using `/script download` command. +* `libraries` - list of libraries or apps to be downloaded when installing the app from the app store. It needs to be a list of map-like resource +descriptors, like the above `resources` field. + ``` + 'libraries' -> [ + { + 'source' -> '/tutorial/carpets.sc' + }, + { + 'source' -> '/fundamentals/heap.sc', + 'target' -> 'heap-lib.sc' + } + ] + ``` + `source` indicates resource location and must point to a scarpet app or library. It can be either an arbitrary url (starting with `http://` + or `https://`), absolute location of a file in the app store (starting with a slash `/`), or a relative location in the same folder as the app + in question (the relative location directly). + `target` is an optional field indicating the new name of the app. If not specified it will place the app into the main data folder with the name it has. +If the app has relative resources dependencies, Carpet will use the app's path for relatives if the app was loaded from the same app store, or none if the +app was loaded from an external url. +If you need to `import()` from dependencies indicated in this block, make sure to have the `__config()` map before any import that references your +remote dependencies, in order to allow them to be downloaded and initialized before the import is executed. +* `'arguments'` - defines custom argument types for legacy commands with `'legacy_command_type_support'` as well +as for the custom commands defined with `'commands'`, see below. +* `'commands'` - defines custom commands for the app to be executed with `/` command, see below. + +## Custom app commands + +Apps can register custom commands added to the existing command system under `/` where `` is the +name of the app. There are three ways apps can provide commands: + +### Simple commands without custom argument support + +Synopsis: +``` +__command() -> 'root command' +foo() -> 'running foo'; +bar(a, b) -> a + b; +baz(a, b) -> // same thing +( + print(a+b); + null +) +``` + +If a loaded app contains `__command()` method, it will attempt to register a command with that app name, +and register all public (not starting with an underscore) functions available in the app as subcommands, in the form of +`/ `. Arguments are parsed from a single +`greedy string` brigadier argument, and split into function parameters. Parsing of arguments is limited +to numbers, string constants, and available global variables, whitespace separated. Using functions and operators other than +unary `-`, would be unsafe, so it is not allowed. +In this mode, if a function returns a non-null value, it will be printed as a result to the +invoker (e.g. in chat). If the provided argument list does not match the expected argument count of a function, an error message +will be generated. + +Running the app command that doesn't take any extra arguments, so `/` will run the `__command() -> ` function. + +This mode is best for quick apps that typically don't require any arguments and want to expose some functionality in a +simple and convenient way. + +### Simple commands with vanilla argument type support + +Synopsis: + ``` +__config() -> {'legacy_command_type_support' -> true}; +__command() -> print('root command'); +foo() -> print('running foo'); +add(first_float, other_float) -> print('sum: '+(first_float+other_float)); +bar(entitytype, item) -> print(entitytype+' likes '+item:0); +baz(entities) -> // same thing + ( + print(join(',',entities)); + ) + ``` + +It works similarly to the auto command, but arguments get their inferred types based on the argument +names, looking at the full name, or any suffix when splitting on `_` that indicates the variable type. For instance, variable named `float` will +be parsed as a floating point number, but it can be named `'first_float'` or `'other_float'` as well. Any variable that is not +supported, will be parsed as a `'string'` type. + +Argument type support includes full support for custom argument types (see below). + +### Custom commands + +Synopsis. This example mimics vanilla `'effect'` command adding extra parameter that is not +available in vanilla - to optionally hide effect icon from UI: +``` +global_instant_effects = {'instant_health', 'instant_damage', 'saturation'}; +__config() -> { + 'commands' -> + { + '' -> _() -> print('this is a root call, does nothing. Just for show'), + 'clear' -> _() -> clear_all([player()]), + 'clear ' -> 'clear_all', + 'clear ' -> 'clear', + 'give ' -> ['apply', -1, 0, false, true], + 'give ' -> ['apply', 0, false, true], + 'give ' -> ['apply', false, true], + 'give ' -> ['apply', true], + 'give ' -> 'apply', + + }, + 'arguments' -> { + 'seconds' -> { 'type' -> 'int', 'min' -> 1, 'max' -> 1000000, 'suggest' -> [60]}, + 'amplifier' -> { 'type' -> 'int', 'min' -> 0, 'max' -> 255, 'suggest' -> [0]}, + 'hideParticles' -> {'type' -> 'bool'}, // pure rename + 'showIcon' -> {'type' -> 'bool'}, // pure rename + } +}; + + +clear_all(targets) -> for(targets, modify(_, 'effect')); +clear(targets, effect) -> for(targets, modify(_, 'effect', effect)); +apply(targets, effect, seconds, amplifier, part, icon) -> +( + ticks = if (has(global_instant_effects, effect), + if (seconds < 0, 1, seconds), + if (seconds < 0, 600, 20*seconds) + ); + for (targets, modify(_, 'effect', effect, ticks, amplifier, part, icon)); +) +``` + +This is the most flexible way to specify custom commands with scarpet. it works by providing command +paths with functions to execute, and optionally, custom argument types. Commands are listed in a map, where +the key (can be empty) consists of +the execution path with the command syntax, which consists of literals (as is) and arguments (wrapped with `<>`), with the name / suffix +of the name of the attribute indicating its type, and the value represent function to call, either function values, +defined function names, or functions with some default arguments. Argument names need to be unique for each command. Values extracted from commands will be passed to the +functions and executed. By default, command list will be checked for ambiguities (commands with the same path up to some point +that further use different attributes), causing app loading error if that happens, however this can be suppressed by specifying +`'allow_command_conflicts'`. + +Unlike with legacy command system with types support, names of the arguments and names of the function parameters don't need to match. +The only important aspect is the argument count and argument order. + +Custom commands provide a substantial subset of brigadier features in a simple package, skipping purposely on some less common +and less frequently used features, like forks and redirects, used pretty much only in the vanilla `execute` command. + +### Command argument types + +Argument types differ from actual argument names that the types are the suffixes of the used argument names, when separated with +`'_'` symbol. For example argument name `'from_pos'` will be interpreted as a built-in type `'int'` and provided to the command system +as a name `'from_pos'`, however if you define a custom type `'from_pos'`, your custom type will be used instead. +Longer suffixes take priority over shorter prefixes, then user defined suffixes mask build-in prefixes. + +There are several default argument types that can be used directly without specifying custom types. + +Each argument can be customized in the `'arguments'` section of the app config, specifying its base type, via `'type'` that needs +to match any of the built-in types, with a series of optional modifiers. Shared modifiers include: + * `suggest` - static list of suggestions to show above the command while typing + * `suggester` - function taking one map argument, indicating current state of attributes in the parsed command + suggesting a dynamic list of valid suggestions for typing. For instance here is a term based type matching + all loaded players adding Steve and Alex, and since player list changes over time cannot be provided statically: + ``` +__config() -> { + 'arguments' -> { + 'loadedplayer' -> { + 'type' -> 'term', + 'suggester' -> _(args) -> ( + nameset = {'Steve', 'Alex'}; + for(player('all'), nameset += _); + keys(nameset) + ), + } + } +}; + ``` + * `case_sensitive` - whether suggestions are case sensitive, defaults to true + +Here is a list of built-in types, with their return value formats, as well as a list of modifiers + that can be customized for that type (if any) + * `'string'`: a string that can be quoted to include spaces. Customizable with `'options'` - a + static list of valid options it can take. command will fail if the typed string is not in this list. + * `'term'`: single word string, no spaces. Can also be customized with `'options'` + * `'text'`: the rest of the command as a string. Has to be the last argument. Can also be customized with `'options'` + * `'bool'`: `true` or `false` + * `'float'`: a number. Customizable with `'min'` and `'max'` values. + * `'int'`: a number, requiring an integer value. Customizable with `'min'` and `'max'` values. + * `'yaw'`: a number, requiring a valid yaw angle. + * `'pos'`: block position as a triple of coordinates. Customized with `'loaded'`, if true requiring the position + to be loaded. + * `'block'`: a valid block state wrapped in a block value (including block properties and data) + * `'blockpredicate`': returns a 4-tuple indicating conditions of a block to match: block name, block tag, + map of required state properties, and tag to match. Either block name or block tag are `null` but not both. + Property map is always specified, but its empty for no conditions, and matching nbt tag can be `null` indicating + no requirements. Technically the 'all-matching' predicate would be `[null, null, {}, null]`, but + block name or block tag is always specified. One can use the following routine to match a block agains this predicate: + ``` + block_to_match = block(x,y,z); + [block_name, block_tag, properties, nbt_tag] = block_predicate; + + (block_name == null || block_name == block_to_match) && + (block_tag == null || block_tags(block_to_match, block_tag)) && + all(properties, block_state(block_to_match, _) == properties:_) && + (!tag || tag_matches(block_data(block_to_match), tag)) + ``` + * `'teamcolor'`: name of a team, and an integer color value of one of 16 valid team colors. + * `'columnpos'`: a pair of x and z coordinates. + * `'dimension'`: string representing a valid dimension in the world. + * `'anchor'`: string of `feet` or `eyes`. + * `'entitytype'`: string representing a type of entity + * `'entities'`: entity selector, returns a list of entities directly. Can be configured with `'single'` to only accept a single entity (will return the entity instead of a singleton) and with `'players'` to only accept players. + * `'floatrange'`: pair of two numbers where one is smaller than the other + * `'players'`: returning a list of valid player name string, logged in or not. If configured with `'single'` returns only one player or `null`. + * `'intrange'`: same as `'floatrange'`, but requiring integers. + * `'enchantment'`: name of an enchantment + * `'slot'`: provides a list of inventory type and slot. Can be configured with `'restrict'` requiring + `'player'`, `'enderchest'`, `'equipment'`, `'armor'`, `'weapon'`, `'container'`, `'villager'` or `'horse'` restricting selection of + available slots. Scarpet supports all vanilla slots, except for `horse.chest` - chest item, not items themselves. This you would + need to manage yourself via nbt directly. Also, for entities that change their capacity, like llamas, you need to check yourself if + the specified container slot is valid for your entity. + * `'item'`: triple of item type, count of 1, and nbt. + * `'message'`: text with expanded embedded player names + * `'effect'`: string representing a status effect + * `'path'`: a valid nbt path + * `'objective'`: a tuple of scoreboard objective name and its criterion + * `'criterion'`: name of a scoreboard criterion + * `'particle'`: name of a particle + * `'recipe'`: name of a valid recipe. Can be fed to recipe_data function. + * `'advancement'`: name of an advancement + * `'lootcondition'`: a loot condition + * `'loottable'`: name of a loot table source + * `'attribute'`: an attribute name + * `'boss'`: a bossbar name + * `'biome'`: a biome name. or biome tag + * `'sound'`: name of a sound + * `'storekey'`: string of a valid current data store key. + * `'identifier'`: any valid identifier. 'minecraft:' prefix is stripped off as a default. + Configurable with `'options'` parameter providing a static list of valid identifiers. + * `'rotation'`: pair of two numbers indicating yaw and pitch values. + * `'scoreholder'`: list of strings of valid score holders. Customizable with `'single'` that makes the parameter require a single target, retuning `null` if its missing + * `'scoreboardslot'` string representing a valid location of scoreboard display. + * `'swizzle'` - set of axis as a string, sorted. + * `'time'` - number of ticks representing a duration of time. + * `'uuid'` - string of a valid uuid. + * `'surfacelocation'` - pair of x and z coordinates, floating point numbers. + * `'location'` - triple of x, y and z coordinates, optionally centered on the block if + interger coordinates are provided and `'block_centered'` optional modifier is `true`. + +## Dimension warning + +One note, which is important is that most of the calls for entities and blocks would refer to the current +dimension of the caller, meaning, that if we for example list all the players using `player('all')` function, +if a player is in the other dimension, calls to entities and blocks around that player would be incorrect. +Moreover, running commandblocks in the spawn chunks would mean that commands will always refer to the overworld +blocks and entities. In case you would want to run commands across all dimensions, just run three of them, +using `/execute in overworld/the_nether/the_end run script run ...` and query players using `player('*')`, +which only returns players in current dimension, or use `in_dimension(expr)` function. diff --git a/docs/scarpet/api/Scoreboard.md b/docs/scarpet/api/Scoreboard.md new file mode 100644 index 0000000..817edd7 --- /dev/null +++ b/docs/scarpet/api/Scoreboard.md @@ -0,0 +1,175 @@ +# Scoreboard + +### `scoreboard()`, `scoreboard(objective)`, `scoreboard(objective, key)`, `scoreboard(objective, key, value)` + +Displays or modifies individual scoreboard values. With no arguments, returns the list of current objectives. +With specified `objective`, lists all keys (players) associated with current objective, or `null` if objective does not exist. +With specified `objective` and +`key`, returns current value of the objective for a given player (key). With additional `value` sets a new scoreboard + value, returning previous value associated with the `key`. If the `value` is null, resets the scoreboard value. + +### `scoreboard_add(objective, criterion?)` + +Adds a new objective to scoreboard. If `criterion` is not specified, assumes `'dummy'`. +Returns `true` if the objective was created, or `null` if an objective with the specified name already exists. + +Throws `unknown_criterion` if criterion doesn't exist. + +
+scoreboard_add('counter')
+scoreboard_add('lvl','level')
+
+ +### `scoreboard_remove(objective)` `scoreboard_remove(objective, key)` + +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)` + +Sets display location for a specified `objective`. If `objective` is `null`, then display is cleared. If objective is invalid, +returns `null`. + +### `scoreboard_property(objective, property)` `scoreboard_property(objective, property, value)` + +Reads a property of an `objective` or sets it to a `value` if specified. Available properties are: + +* `criterion` +* `display_name` (Formatted text supported) +* `display_slot`: When reading, returns a list of slots this objective is displayed in, when modifying, displays the objective in the specified slot +* `render_type`: Either `'integer'` or `'hearts'`, defaults to `'integer'` if invalid value specified + +# Team + +### `team_list()`, `team_list(team)` + +Returns all available teams as a list with no arguments. + +When a `team` is specified, it returns all the players inside that team. If the `team` is invalid, returns `null`. + +### `team_add(team)`, `team_add(team,player)` + +With one argument, creates a new `team` and returns its name if successful, or `null` if team already exists. + + +`team_add('admin')` -> Create a team with the name 'admin' +`team_add('admin','Steve')` -> Joing the player 'Steve' into the team 'admin' + +If a `player` is specified, the player will join the given `team`. Returns `true` if player joined the team, or `false` if nothing changed since the player was already in this team. If the team is invalid, returns `null` + +### `team_remove(team)` + +Removes a `team`. Returns `true` if the team was deleted, or `null` if the team is invalid. + +### `team_leave(player)` + +Removes the `player` from the team he is in. Returns `true` if the player left a team, otherwise `false`. + +`team_leave('Steve')` -> Removes Steve from the team he is currently in +`for(team_list('admin'), team_leave('admin', _))` -> Remove all players from team 'admin' + +### `team_property(team,property,value?)` + +Reads the `property` of the `team` if no `value` is specified. If a `value` is added as a third argument, it sets the `property` to that `value`. + +* `collisionRule` + * Type: String + * Options: always, never, pushOtherTeams, pushOwnTeam + +* `color` + * Type: String + * Options: See [team command](https://minecraft.wiki/w/Commands/team#Arguments) (same strings as `'teamcolor'` [command argument](https://github.com/gnembon/fabric-carpet/blob/master/docs/scarpet/Full.md#command-argument-types) options) + +* `displayName` + * Type: String or FormattedText, when querying returns FormattedText + +* `prefix` + * Type: String or FormattedText, when querying returns FormattedText + +* `suffix` + * Type: String or FormattedText, when querying returns FormattedText + +* `friendlyFire` + * Type: boolean + +* `seeFriendlyInvisibles` + * Type: boolean + +* `nametagVisibility` + * Type: String + * Options: always, never, hideForOtherTeams, hideForOwnTeam + +* `deathMessageVisibility` + * Type: String + * Options: always, never, hideForOtherTeams, hideForOwnTeam + +Examples: + +``` +team_property('admin','color','dark_red') Make the team color for team 'admin' dark red +team_property('admin','prefix',format('r Admin | ')) Set prefix of all players in 'admin' +team_property('admin','display_name','Administrators') Set display name for team 'admin' +team_property('admin','seeFriendlyInvisibles',true) Make all players in 'admin' see other admins even when invisible +team_property('admin','deathMessageVisibility','hideForOtherTeams') Make all players in 'admin' see other admins even when invisible +``` + +## `bossbar()`, `bossbar(id)`, `bossbar(id,property,value?)` + +Manage bossbars just like with the `/bossbar` command. + +Without any arguments, returns a list of all bossbars. + +When an id is specified, creates a bossbar with that `id` and returns the id of the created bossbar. +Bossbar ids need a namespace and a name. If no namespace is specified, it will automatically use `minecraft:`. +In that case you should keep track of the bossbar with the id that `bossbar(id)` returns, because a namespace may be added automatically. +If the id was invalid (for example by having more than one colon), returns `null`. +If the bossbar already exists, returns `false`. + +`bossbar('timer') => 'minecraft:timer'` (Adds the namespace `minecraft:` because none is specified) + +`bossbar('scarpet:test') => 'scarpet:test'` In this case there is already a namespace specified + +`bossbar('foo:bar:baz') => null` Invalid identifier + +`bossbar(id,property)` is used to query the `property` of a bossbar. + +`bossbar(id,property,value)` can modify the `property` of the bossbar to a specified `value`. + +Available properties are: + +* color: can be `'pink'`, `'blue'`, `'red'`, `'green'`, `'yellow'`, `'purple'` or `'white'` + +* style: can be `'progress'`, `'notched_6'`, `'notched_10'`, `'notched_12'` or `'notched_20'` + +* value: value of the bossbar progress + +* max: maximum value of the bossbar progress, by default 100 + +* name: Text to display above the bossbar, supports formatted text + +* visible: whether the bossbar is visible or not + +* players: List of players that can see the bossbar + +* add_player: add a player to the players that can see this bossbar, this can only be used for modifying (`value` must be present) + +* remove: remove this bossbar, no `value` required + +``` +bossbar('script:test','style','notched_12') +bossbar('script:test','value',74) +bossbar('script:test','name',format('rb Test')) -> Change text +bossbar('script:test','visible',false) -> removes visibility, but keeps players +bossbar('script:test','players',player('all')) -> Visible for all players +bossbar('script:test','players',player('Steve')) -> Visible for Steve only +bossbar('script:test','players',null) -> Invalid player, removing all players +bossbar('script:test','add_player',player('Alex')) -> Add Alex to the list of players that can see the bossbar +bossbar('script:test','remove') -> remove bossbar 'script:test' +for(bossbar(),bossbar(_,'remove')) -> remove all bossbars +``` + + + + diff --git a/docs/scarpet/api/ScriptCommand.md b/docs/scarpet/api/ScriptCommand.md new file mode 100644 index 0000000..fb4a8b9 --- /dev/null +++ b/docs/scarpet/api/ScriptCommand.md @@ -0,0 +1,184 @@ +# `/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 +the executing entity of the command. You will receive tab completion suggestions as you type your code suggesting +functions and global variables. It is advisable to use `/execute in ... at ... as ... run script run ...` or similar, +to simulate running commands in a different scope. + +# `/script load / unload (global?)`, `/script in ` commands + +`load / unload` commands allow for very convenient way of writing your code, providing it to the game and +distribute with your worlds without the need of use of commandblocks. Just place your Scarpet code in the +`/scripts` folder of your world files and make sure it ends with `.sc` extension. In singleplayer, you can +also save your scripts in `.minecraft/config/carpet/scripts` to make them available in any world. + +The good thing about editing that code is that you can not only use normal editing without the need of marking of newlines, +but you can also use comments in your code. + +A comment is anything that starts with a double slash, and continues to the end of the line: + +
+foo = 1;
+//This is a comment
+bar = 2;
+// This never worked, so I commented it out
+// baz = foo()
+
+ +### `/script load/unload (?global)` + +Loading operation will load that script code from disk and execute it right away. You would probably use it to load +some stored procedures to be used for later. To reload the module, just type `/script load` again. Reloading removes +all the current global state (globals and functions) that were added later by the module. To reload all apps along with +all game resources, use vanilla `/reload` command. + + + +Loaded apps have the ability to store and load external files, especially their persistent tag state. For that +check `load_app_data` and `store_app_data` functions. + + + +Unloading the app will only mask their command tree, not remove it. This has the same effect than not having that command +at all, with the exception that if you load a different app with the same name, this may cause commands to reappear. +To remove the commands fully, use `/reload`. + + + +### `/script in ...` + +Allows to run normal /script commands in a specific app, like `run, invoke,..., globals` etc... + +# `/script invoke / invokepoint / invokearea`, `/script globals` commands + +`invoke` family of commands provide convenient way to invoke stored procedures (i.e. functions that has been +defined previously by any running script. To view current stored procedure set, +run `/script globals`(or `/script globals all` to display all functions even hidden ones), to define a new stored +procedure, just run a `/script run function(a,b) -> ( ... )` command with your procedure once, and to forget a +procedure, use `undef` function: `/script run undef('function')` + +### `/script invoke ...` + +Equivalent of running `/script run fun(args, ...)`, but you get the benefit of getting the tab completion of the +command name, and lower permission level required to run these (since player is not capable of running any custom +code in this case, only this that has been executed before by an operator). Arguments will be checked for validity, +and you can only pass simple values as arguments (strings, numbers, or `null` value). Use quotes to include +whitespaces in argument strings. + +Command will check provided arguments with required arguments (count) and fail if not enough or too much +arguments are provided. Operators defining functions are advised to use descriptive arguments names, as these +will be visible for invokers and form the base of understanding what each argument does. + +`invoke` family of commands will tab complete any stored function that does not start with `'_'`, it will still +allow to run procedures starting with `'_'` but not suggest them, and ban execution of any hidden stored procedures, +so ones that start with `'__'`. In case operator needs to use subroutines for convenience and don't want to expose +them to the `invoke` callers, they can use this mechanic. + +
+/script run example_function(const, phrase, price) -> print(const+' '+phrase+' '+price)
+/script invoke example_function pi costs 5
+
+ +### `/script invokepoint ...` + +It is equivalent to `invoke` except it assumes that the first three arguments are coordinates, and provides +coordinates tab completion, with `looking at...` mechanics for convenience. All other arguments are expected +at the end + +### `/script invokearea ...` + +It is equivalent to `invoke` except it assumes that the first three arguments are one set of coordinates, +followed by the second set of coordinates, providing tab completion, with `looking at...` mechanics for convenience, +followed by any other required arguments + +# `/script scan`, `/script fill` and `/script outline` commands + +These commands can be used to evaluate an expression over an area of blocks. They all need to have specified the +origin of the analyzed area (which is used as referenced (0,0,0), and two corners of an area to analyzed. If you +would want that the script block coordinates refer to the actual world coordinates, use origin of `0 0 0`, or if +it doesn't matter, duplicating coordinates of one of the corners is the easiest way. + +These commands, unlike raw `/script run` are limited by vanilla fill / clone command limit of 32k blocks, which can +be altered with carpet mod's own `/carpet fillLimit` command. + +### `/script scan origin corner corner expr` + +Evaluates expression for each point in the area and returns number of successes (result was positive). Since the +command by itself doesn't affect the area, the effects would be in side effects. + +### `/script fill origin corner corner "expr" (? replace )` + +Think of it as a regular fill command, that sets blocks based on whether a result of the command was successful. +Note that the expression is in quotes. Thankfully string constants in `scarpet` use single quotes. Can be used to +fill complex geometric shapes. + +### `/script outline origin corner corner "expr" (? replace )` + +Similar to `fill` command it evaluates an expression for each block in the area, but in this case setting blocks +where condition was true and any of the neighbouring blocks were evaluated negatively. This allows to create surface +areas, like sphere for example, without resorting to various rounding modes and tricks. + +Here is an example of seven ways to draw a sphere of radius of 32 blocks around 0 100 0: + +
+/script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <  32*32" white_stained_glass replace air
+/script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <= 32*32" white_stained_glass replace air
+/script outline 0 100 0 -40 60 -40 40 140 40 "x*x+y*y+z*z <  32.5*32.5" white_stained_glass replace air
+/script fill    0 100 0 -40 60 -40 40 140 40 "floor(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+/script fill    0 100 0 -40 60 -40 40 140 40 "round(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+/script fill    0 100 0 -40 60 -40 40 140 40 "ceil(sqrt(x*x+y*y+z*z)) == 32" white_stained_glass replace air
+/draw sphere 0 100 0 32 white_stained_glass replace air // fluffy ball round(sqrt(x*x+y*y+z*z)-rand(abs(y)))==32
+
+ +The last method is the one that world edit is using (part of carpet mod). It turns out that the outline method +with `32.5` radius, fill method with `round` function and draw command are equivalent + +# `script stop/script resume` command + +`/script stop` allows to stop execution of any script currently running that calls the `game_tick()` function which +allows the game loop to regain control of the game and process other commands. This will also make sure that all +current and future programs will stop their execution. Execution of all programs will be prevented +until `/script resume` command is called. + +Lets look at the following example. This is a program computes Fibonacci number in a recursive manner: + +
+fib(n) -> if(n<3, 1, fib(n-1)+fib(n-2) ); fib(8)
+
+ +That's really bad way of doing it, because the higher number we need to compute the compute requirements will +rise exponentially with `n`. It takes a little over 50 milliseconds to do fib(24), so above one tick, but about +a minute to do fib(40). Calling fib(40) will not only freeze the game, but also you woudn't be able to interrupt +its execution. We can modify the script as follows + +
+fib(n) -> ( game_tick(50); if(n<3, 1, fib(n-1)+fib(n-2) ) ); fib(40)
+
+ +But this would never finish as such call would finish after `~ 2^40` ticks. To make our computations responsive, +yet able to respond to user interactions, other commands, as well as interrupt execution, we could do the following: + +
+fib(n) -> ( if(n==23, game_tick(50) ); if(n<3, 1, fib(n-1)+fib(n-2) ) ); fib(40)
+
+ +This would slow down the computation of fib(40) from a minute to two, but allows the game to keep continue running +and be responsive to commands, using about half of each tick to advance the computation. Obviously depending on the +problem, and available hardware, certain things can take more or less time to execute, so portioning of work with +calling `gametick` should be balanced in each case separately + +# `/script download` command + +`/script download ` command allows downloading and running apps directly from an online app store (it's all free), +by default the [scarpet app store](https://www.github.com/gnembon/scarpet). +Downloaded apps will be placed in the world's scripts folder automatically. Location of the app store is controlled +with a global carpet setting of `/carpet scriptsAppStore`. Apps, if required, will also download all the resources they need +to run it. Consecutive downloads of the same app will re-download its content and its resources, but will not remove anything +that has been removed or renamed. + +# `/script remove` command + +command allow to stop and remove apps installed in the worlds scripts folder. The app is unloaded and app 'sc' file is moved +to the `/scripts/trash`. Removed apps can only be restored by manually moving it back from the trash folder, +or by redownloading from the appstore. diff --git a/docs/scarpet/language/Containers.md b/docs/scarpet/language/Containers.md new file mode 100644 index 0000000..a5c92b3 --- /dev/null +++ b/docs/scarpet/language/Containers.md @@ -0,0 +1,272 @@ +# Lists, Maps and API support for Containers + +Scarpet supports basic container types: lists and maps (aka hashmaps, dicts etc..) + +## Container manipulation + +Here is a list of operations that work on all types of containers: lists, maps, as well as other Minecraft specific +modifyable containers, like NBTs + +### `get(container, address, ...), get(lvalue), ':' operator` + +Returns the value at `address` element from the `value`. For lists it indicates an index, use negative numbers to +reach elements from the end of the list. `get` call will always be able to find the index. In case there is few +items, it will loop over + +for maps, retrieves the value under the key specified in the `address` or null otherwise + +[Minecraft specific usecase]: In case `value` is of `nbt` type, uses address as the nbt path to query, returning null, +if path is not found, one value if there was one match, or list of values if result is a list. Returned elements can +be of numerical type, string texts, or another compound nbt tags + +In case to simplify the access with nested objects, you can add chain of addresses to the arguments of `get` rather +than calling it multiple times. `get(get(foo,a),b)` is equivalent to `get(foo, a, b)`, or `foo:a:b`. + +
+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({ 'foo' -> 2, 'bar' -> 3, 'baz' -> 4 }, 'bar')  => 3
+
+ +### `has(container, address, ...), has(lvalue)` + +Similar to `get`, but returns boolean value indicating if the given index / key / path is in the container. +Can be used to determine if `get(...)==null` means the element doesn't exist, or the stored value for this +address is `null`, and is cheaper to run than `get`. + +Like get, it can accept multiple addresses for chains in nested containers. In this case `has(foo:a:b)` is +equivalent to `has(get(foo,a), b)` or `has(foo, a, b)` + +### `delete(container, address, ...), delete(lvalue)` + +Removes specific entry from the container. For the lists - removes the element and shrinks it. For maps, it +removes the key from the map, and for nbt - removes content from a given path. + +Like with the `get` and `has`, `delete` can accept chained addresses, as well as l-value container access, removing +the value from the leaf of the path provided, so `delete(foo, a, b)` is the +same as `delete(get(foo,a),b)` or `delete(foo:a:b)` + +Returns true, if container was changed, false, if it was left unchanged, and null if operation was invalid. + +### `put(container, address, value), put(container, address, value, mode), put(lvalue, value)` + +**Lists** + +Modifies the container by replacing the value under the address with the supplied `value`. For lists, a valid +index is required, but can be negative as well to indicate positions from the end of the list. If `null` is +supplied as the address, it always means - add to the end of the list. + +There are three modes that lists can have items added to them: + +* `replace`(default): Replaces item under given index(address). Doesn't change the size of the array +unless `null` address is used, which is an exception and then it appends to the end +* `insert`: Inserts given element at a specified index, shifting the rest of the array to make space for the item. +Note that index of -1 points to the last element of the list, thus inserting at that position and moving the previous +last element to the new last element position. To insert at the end, use `+=` operator, or `null` address in put +* `extend`: treats the supplied value as an iterable set of values to insert at a given index, extending the list +by this amount of items. Again use `null` address/index to point to the end of the list + +Due to the extra mode parameter, there is no chaining for `put`, but you can still use l-value container access to +indicate container and address, so `put(foo, key, value)` is the same as `put(foo:key, value)` or `foo:key=value` + +Returns true, if container got modified, false otherwise, and null if operation was invalid. + +**Maps** + +For maps there are no modes available (yet, seems there is no reason to). It replaces the value under the supplied +key (address), or sets it if not currently present. + +**NBT Tags** + +The address for nbt values is a valid nbt path that you would use with `/data` command, and tag is any tag that +would be applicable for a given insert operation. Note that to distinguish between proper types (like integer types, +you need to use command notation, i.e. regular ints is `123`, while byte size int would be `123b` and an explicit +string would be `"5"`, so it helps that scarpet uses single quotes in his strings. Unlike for lists and maps, it +returns the number of affected nodes, or 0 if none were affected. + +There are three modes that NBT tags can have items added to them: + +* `replace`(default): Replaces item under given path(address). Removes them first if possible, and then adds given +element to the supplied position. The target path can indicate compound tag keys, lists, or individual elements +of the lists. +* ``: Index for list insertions. Inserts given element at a specified index, inside a list specified with the +path address. Fails if list is not specified. It behaves like `insert` mode for lists, i.e. it is not removing any +of the existing elements. Use `replace` to remove and replace existing element. +* `merge`: assumes that both path and replacement target are of compound type (dictionaries, maps, `{}` types), +and merges keys from `value` with the compound tag under the path + +
+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
+     => {a:[{lvl:[1,2,1,3]},{lvl:[3,2,1,1]},{lvl:[4,5,1,6]}]}
+tag = nbt('{a:[{lvl:[1,2,3]},{lvl:[3,2,1]},{lvl:[4,5,6]}]}'); put(tag, 'a[].lvl[1]', 1); tag
+     => {a:[{lvl:[1,1,3]},{lvl:[3,1,1]},{lvl:[4,1,6]}]}
+
+ +## List operations + +### `[value, ...?]`,`[iterator]`,`l(value, ...?)`, `l(iterator)` + +Creates a list of values of the expressions passed as parameters. It can be used as an L-value and if all +elements are variables, you coujld use it to return multiple results from one function call, if that +function returns a list of results with the same size as the `[]` call uses. In case there is only one +argument and it is an iterator (vanilla expression specification has `range`, but Minecraft API implements +a bunch of them, like `diamond`), it will convert it to a proper list. Iterators can only be used in high order +functions, and are treated as empty lists, unless unrolled with `[]`. + +Internally, `[elem, ...]`(list syntax) and `l(elem, ...)`(function syntax) are equivalent. `[]` is simply translated to +`l()` in the scarpet preprocessing stage. This means that internally the code has always expression syntax despite `[]` +not using different kinds of brackets and not being proper operators. This means that `l(]` and `[)` are also valid +although not recommended as they will make your code far less readable. + +
+l(1,2,'foo') <=> [1, 2, 'foo']
+l() <=> [] (empty list)
+[range(10)] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+[1, 2] = [3, 4] => Error: l is not a variable
+[foo, bar] = [3, 4]; foo==3 && bar==4 => 1
+[foo, bar, baz] = [2, 4, 6]; [min(foo, bar), baz] = [3, 5]; [foo, bar, baz]  => [3, 4, 5]
+
+ +In the last example `[min(foo, bar), baz]` creates a valid L-value, as `min(foo, bar)` finds the lower of the +variables (in this case `foo`) creating a valid assignable L-list of `[foo, baz]`, and these values +will be assigned new values + +### `join(delim, list), join(delim, values ...)` + +Returns a string that contains joined elements of the list, iterator, or all values, +concatenated with `delim` delimiter + +
+join('-',range(10))  => 0-1-2-3-4-5-6-7-8-9
+join('-','foo')  => foo
+join('-', 'foo', 'bar')  => foo-bar
+
+ +### `split(delim?, expr)` + +Splits a string under `expr` by `delim` which can be a regular expression. If no delimiter is specified, it splits +by characters. + +If `expr` is a list, it will split the list into multiple sublists by the element (s) which equal `delim`, or which equal the empty string +in case no delimiter is specified. + +Splitting a `null` value will return an empty list. + +
+split('foo') => [f, o, o]
+split('','foo')  => [f, o, o]
+split('.','foo.bar')  => []
+split('\\.','foo.bar')  => [foo, bar]
+split(1,[2,5,1,2,3,1,5,6]) => [[2,5],[2,3],[5,6]]
+split(1,[1,2,3,1,4,5,1] => [[], [2,3], [4,5], []]
+split(null) => []
+
+ +### `slice(expr, from, to?)` + +extracts a substring, or sublist (based on the type of the result of the expression under expr with +starting index of `from`, and ending at `to` if provided, or the end, if omitted. Can use negative indices to +indicate counting form the back of the list, so `-1 <=> length(_)`. + +Special case is made for iterators (`range`, `rect` etc), which does require non-negative indices (negative `from` is treated as +`0`, and negative `to` as `inf`), but allows retrieving parts of the sequence and ignore +other parts. In that case consecutive calls to `slice` will refer to index `0` the current iteration position since iterators +cannot go back nor track where they are in the sequence (see examples). + +
+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]
+slice(range(10), 5)  => [5, 6, 7, 8, 9]
+r = range(100); [slice(r, 5, 7), slice(r, 1, 3)]  => [[5, 6], [8, 9]]
+
+ +### `sort(list), sort(values ...)` + +Sorts in the default sortographical order either all arguments, or a list if its the only argument. +It returns a new sorted list, not affecting the list passed to the argument + +
sort(3,2,1)  => [1, 2, 3]
+sort('a',3,11,1)  => [1, 3, 11, 'a']
+list = [4,3,2,1]; sort(list)  => [1, 2, 3, 4]
+
+ +### `sort_key(list, key_expr)` + +Sorts a copy of the list in the order or keys as defined by the `key_expr` for each element + +
+sort_key([1,3,2],_)  => [1, 2, 3]
+sort_key([1,3,2],-_)  => [3, 2, 1]
+sort_key([range(10)],rand(1))  => [1, 0, 9, 6, 8, 2, 4, 5, 7, 3]
+sort_key([range(20)],str(_))  => [0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9]
+
+ +### `range(to), range(from, to), range(from, to, step)` + +Creates a range of numbers from `from`, no greater/larger than `to`. The `step` parameter dictates not only the +increment size, but also direction (can be negative). The returned value is not a proper list, just the iterator +but if for whatever reason you need a proper list with all items evaluated, use `[range(to)]`. +Primarily to be used in higher order functions + +
+range(10)  => [...]
+[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]
+range(20, 10, -2)  => [20, 18, 16, 14, 12]
+range(-0.3, 0.3, 0.1)  => [-0.3, -0.2, -0.1, 0, 0.1, 0.2]
+range(0.3, -0.3, -0.1) => [0.3, 0.2, 0.1, -0, -0.1, -0.2]
+
+ +## Map operations + +Scarpet supports map structures, aka hashmaps, dicts etc. Map structure can also be used, with `null` values as sets. +Apart from container access functions, (`. , get, put, has, delete`), the following functions: + +### `{values, ...}`,`{iterator}`,`{key -> value, ...}`,`m(values, ...)`, `m(iterator)`, `m(l(key, value), ...))` + +creates and initializes a map with supplied keys, and values. If the arguments contains a flat list, these are all +treated as keys with no value, same goes with the iterator - creates a map that behaves like a set. If the +arguments is a list of lists, they have to have two elements each, and then first is a key, and second, a value + +In map creation context (directly inside `{}` or `m{}` call), `->` operator acts like a pair constructor for simpler +syntax providing key value pairs, so the invocation to `{foo -> bar, baz -> quux}` is equivalent to +`{[foo, bar], [baz, quux]}`, which is equivalent to somewhat older, but more traditional functional form of +`m(l(foo, bar),l(baz, quuz))`. + +Internally, `{?}`(list syntax) and `m(?)`(function syntax) are equivalent. `{}` is simply translated to +`m()` in the scarpet preprocessing stage. This means that internally the code has always expression syntax despite `{}` +not using different kinds of brackets and not being proper operators. This means that `m(}` and `{)` are also valid +although not recommended as they will make your code far less readable. + +When converting map value to string, `':'` is used as a key-value separator due to tentative compatibility with NBT +notation, meaning in simpler cases maps can be converted to NBT parsable string by calling `str()`. This however +does not guarantee a parsable output. To properly convert to NBT value, use `encode_nbt()`. + +
+{1, 2, 'foo'} => {1: null, 2: null, foo: null}
+m() <=> {} (empty map)
+{range(10)} => {0: null, 1: null, 2: null, 3: null, 4: null, 5: null, 6: null, 7: null, 8: null, 9: null}
+m(l(1, 2), l(3, 4)) <=> {1 -> 2, 3 -> 4} => {1: 2, 3: 4}
+reduce(range(10), put(_a, _, _*_); _a, {})
+     => {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
+
+ +### `keys(map), values(map), pairs(map)` + +Returns full lists of keys, values and key-value pairs (2-element lists) for all the entries in the map diff --git a/docs/scarpet/language/FunctionsAndControlFlow.md b/docs/scarpet/language/FunctionsAndControlFlow.md new file mode 100644 index 0000000..f42017a --- /dev/null +++ b/docs/scarpet/language/FunctionsAndControlFlow.md @@ -0,0 +1,318 @@ +# User-defined functions and program control flow + +## Writing programs with more than 1 line + +### Operator `;`, `then(...)` + +To effectively write programs that have more than one line, a programmer needs way to specify a sequence of commands +that execute one after another. In `scarpet` this can be achieved with `;`. Its an operator, and by separating +statements with semicolons. And since whitespaces and +`$` (commandline visible newline separator) +sign are all treats as whitespaces, how you layout your code doesn't matter, as long as it is readable to everyone involved. + +
+expr;
+expr;
+expr;
+expr
+
+ +Notice that the last expression is not followed by a semicolon. Since instruction separation is functional +in `scarpet`, and not barely an instruction delimiter, terminating the code with a dangling operator wouldn't +be valid. Having said that, since many programming languages don't care about the number of op terminators +programmers use, carpet preprocessor will remove all unnecessary semicolons from scripts when compiled. + +In general `expr; expr; expr; expr` is equivalent to `(((expr ; expr) ; expr) ; expr)` or `then(expr, expr, expr, expr)`. + +Result of the evaluated expression is the same as the result of the second expression, but first expression +is also evaluated for side-effects + +
+expr1 ; expr2 => expr2  // with expr1 as a side-effect
+
+ +## Global variables + +All defined functions are compiled, stored persistently, and available globally within the app. +Functions can only be undefined via call to `undef('fun')`, which would erase global entry for function `fun`. +Since all variables have local scope inside each function, or each command script, + global variables is a way to share the global state. + +Any variable that is used with a name that starts with `'global_'` will be stored and accessible globally, not only +inside the current scope. If used directly in the chat window with the default app, it will persist across calls to `/script` +function. Like functions, which are global, global variables can only be undefined via `undef`. + +For apps running in `'global'` scope - all players will share the same global variables and defined functions, +and with `player` scope, each player hosts its own state for each app, so function and global_variables are distinct. + + +
+/script run a() -> global_list+=1; global_list = [1,2,3]; a(); a(); global_list  // => [1, 2, 3, 1, 1]
+/script run a(); a(); global_list  // => [1, 2, 3, 1, 1, 1, 1]
+
+ +### `Operator ->` + +`->` operator has two uses - as a function definition operator and key-value initializer for maps. + +To organize code better than a flat sequence of operations, one can define functions. Definition is correct +if has the following form + +
+fun(args, ...) -> expr
+
+ +Where `fun(args, ...)` is a function signature indicating function name, number of arguments, and their names, +and expr is an expression (can be complex) that is evaluated when `fun` is called. Names in the signature don't +need to be used anywhere else, other occurrences of these names will be masked in this function scope. Function +call creates new scope for variables inside `expr`, so all non-global variables are not visible from the caller +scope. All parameters are passed by value to the new scope, including lists and other containers, however their +copy will be shallow. + +The function returns itself as a first class object, which means it can be used to call it later with the `call` function + +Using `_` as the function name creates anonymous function, so each time `_` function is defined, it will be given +a unique name, which you can pass somewhere else to get this function `call`ed. Anonymous functions can only be called +by their value and `call` method. + +
+a(lst) -> lst+=1; list = [1,2,3]; a(list); a(list); list  // => [1,2,3]
+
+ +In case the inner function wants to operate and modify larger objects, lists from the outer scope, but not global, +it needs to use `outer` function in function signature. + +in map construction context (directly in `m()` or `{}`), the `->` operator has a different function by converting its +arguments to a tuple which is used by map constructor as a key-value pair: + +
+{ 'foo' -> 'bar' } => {l('foo', 'bar')}
+
+ +This means that it is not possible to define literally a set of inline function, however a set of functions can still +be created by adding elements to an empty set, and building it this way. That's a tradeoff for having a cool map initializer. + +### `outer(arg)` + +`outer` function can only be used in the function signature, and it will cause an error everywhere else. It +saves the value of that variable from the outer scope and allows its use in the inner scope. This is a similar +behaviour to using outer variables in lambda function definitions from Java, except here you have to specify +which variables you want to use, and borrow + +This mechanism can be used to use static mutable objects without the need of using `global_...` variables + +
+list = [1,2,3]; a(outer(list)) -> list+=1;  a(); a(); list  // => [1,2,3,1,1]
+
+ +The return value of a function is the value of the last expression. This as the same effect as using outer or +global lists, but is more expensive + +
+a(lst) -> lst+=1; list = [1,2,3]; list=a(list); list=a(list); list  // => [1,2,3,1,1]
+
+ +Ability to combine more statements into one expression, with functions, passing parameters, and global and outer +scoping allow to organize even larger scripts + +### `Operator ...` + +Defines a function argument to represent a variable length argument list of whatever arguments are left +from the argument call list, also known as a varargs. There can be only one defined vararg argument in a function signature. +Technically, it doesn't matter where is it, but it looks best at the butt side of it. + +
+foo(a, b, c) -> ...  # fixed argument function, call foo(1, 2, 3)
+foo(a, b, ... c) -> ... # c is now representing the variable argument part
+    foo(1, 2)  # a:1, b:2, c:[]
+    foo(1, 2, 3)  # a:1, b:2, c:[3]
+    foo(1, 2, 3, 4)  # a:1, b:2, c:[3, 4] 
+foo(... x) -> ...  # all arguments for foo are included in the list
+
+    
+
+ +### `import(module_name, ? symbols ...)` + +Imports symbols from other apps and libraries into the current one: global variables or functions, allowing to use +them in the current app. This includes other symbols imported by these modules. Scarpet supports circular dependencies, +but if symbols are used directly in the module body rather than functions, it may not be able to retrieve them. + +Returns full list of available symbols that could be imported from this module, which can be used to debug import +issues, and list contents of libraries. + +You can load and import functions from dependencies in a remote app store's source specified in your config's `libraries` block, but make sure +to place your config _before_ the import in order to allow the remote dependency to be downloaded (currently, app resources are only downloaded +when using the `/carpet download` command). + +### `call(function, ? args ...)` + +calls a user defined function with specified arguments. It is equivalent to calling `function(args...)` directly +except you can use it with function value, or name instead. This means you can pass functions to other user defined +functions as arguments and call them with `call` internally. Since function definitions return the defined +function, they can be defined in place as anonymous functions. + +#### Passing function references to other modules of your application + +In case a function is defined by its name, Scarpet will attempt to resolve its definition in the given module and its imports, +meaning if the call is in a imported library, and not in the main module of your app, and that function is not visible from the +library perspective, but in the app, it won't be call-able. In case you pass a function name to a separate module in your app, +it should import back that method from the main module for visibility. + +Check an example of a problematic code of a library that expects a function value as a passed argument and how it is called in +the parent app: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme('foo' , 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` + +In this case `'foo'` will fail to dereference in `lib` as it is not visible by name. In tightly coupled modules, where `lib` is just +a component of your `app` you can use circular import to acknowledge the symbol from the other module (pretty much like +imports in Java classes), and that solves the issue but makes the library dependent on the main app: +``` +//lib.scl +import('app','foo'); +callme(fun, arg) -> call(fun, arg); +``` +You can circumvent that issue by explicitly dereferencing the local function where it is used as a lambda argument created +in the module in which the requested function is visible: +``` +//app.sc +import('lib', 'callme'); +foo(x) -> x*x; +test() -> callme(_(x) -> foo(x), 5); +``` +``` +//lib.scl +callme(fun, arg) -> call(fun, arg); +``` +Or by passing an explicit reference to the function, instead of calling it by name: +``` +//app.sc +import('lib', 'callme'); +global_foohandler = (foo(x) -> x*x); +test() -> callme(global_foohandler, 5); +``` + +Little technical note: the use of `_` in expression passed to built in functions is much more efficient due to not +creating new call stacks for each invoked function, but anonymous functions is the only mechanism available for +programmers with their own lambda arguments + +
+my_map(list, function) -> map(list, call(function, _));
+my_map([1,2,3], _(x) -> x*x);    // => [1,4,9]
+profile_expr(my_map([1,2,3], _(x) -> x*x));   // => ~32000
+sq(x) -> x*x; profile_expr(my_map([1,2,3], 'sq'));   // => ~36000
+sq = (_(x) -> x*x); profile_expr(my_map([1,2,3], sq));   // => ~36000
+profile_expr(map([1,2,3], _*_));   // => ~80000
+
+ +## Control flow + +### `return(expr?)` + +Sometimes its convenient to break the organized control flow, or it is not practical to pass the final result value of +a function to the last statement, in this case a return statement can be used + +If no argument is provided - returns null value. + +
+def() -> (
+   expr1;
+   expr2;
+   return(expr3); // function terminates returning expr3
+   expr4;     // skipped
+   expr5      // skipped
+)
+
+ +In general its cheaper to leave the last expression as a return value, rather than calling +returns everywhere, but it would often lead to a messy code. + +### `exit(expr?)` + +It terminates entire program passing `expr` as the result of the program execution, or null if omitted. + +### `try(expr)` `try(expr, user_catch_expr)` `try(expr, type, catch_expr, type?, catch_expr?, ...)` + +`try` evaluates expression, allowing capturing exceptions that would be thrown inside `expr` statement. The exceptions can be +thrown explicitly using `throw()` or internally by scarpet where code is correct but detects illegal state. The 2-argument form +catches only user-thrown exceptions and one argument call `try(expr)` is equivalent to `try(expr, null)`, +or `try(expr, 'user_exception', null)`. If multiple `type-catch` pairs are defined, the execution terminates on the first +applicable type for the exception thrown. Therefore, even if the caught exception matches multiple filters, only +the first matching block will be executed. + +Catch expressions are evaluated with +`_` set to the value associated with the exception and `_trace` set to contain details about point of error (token, and line and +column positions), call stack and local +variables at the time of failure. The `type` will catch any exception of that type and any subtype of this type. + + +You can use `try` mechanism to exit from large portion of a convoluted call stack and continue program execution, although catching +exceptions is typically much more expensive comparing to not throwing them. + +The `try` function allows you to catch some scarpet exceptions for cases covering invalid data, like invalid +blocks, biomes, dimensions and other things, that may have been modified by datapacks, resourcepacks or other mods, +or when an error is outside of the programmers scope, such as problems when reading or decoding files. + +This is the hierarchy of the exceptions that could be thrown/caught in the with the `try` function: +- `exception`: This is the base exception. Catching `'exception'` allows to catch everything that can be caught, +but like everywhere else, doing that sounds like a bad idea. + - `value_exception`: This is the parent for any exception that occurs due to an + incorrect argument value provided to a built-in function + - `unknown_item`, `unknown_block`, `unknown_biome`, `unknown_sound`, `unknown_particle`, + `unknown_poi_type`, `unknown_dimension`, `unknown_structure`, `unknown_criterion`: Specific + errors thrown when a specified internal name does not exist or is invalid. + - `io_exception`: This is the parent for any exception that occurs due to an error handling external data. + - `nbt_error`: Incorrect input/output NBT file. + - `json_error`: Incorrect input/output JSON data. + - `b64_error`: Incorrect input/output b64 (base 64) string + - `user_exception`: Exception thrown by default with `throw` function. + +Synopsis: +
+inner_call() ->
+(
+   aaa = 'booyah';
+   try(
+      for (range(10), item_tags('stick'+_*'k'));
+   ,
+      print(_trace) // not caught, only catching user_exceptions
+   )
+);
+
+outer_call() -> 
+( 
+   try(
+      inner_call()
+   , 'exception', // catching everything
+      print(_trace)
+   ) 
+);
+
+Producing: +``` +{stack: [[, inner_call, 1, 14]], locals: {_a: 0, aaa: booyah, _: 1, _y: 0, _i: 1, _x: 0, _z: 0}, token: [item_tags, 5, 23]} +``` + +### `throw(value?)`, `throw(type, value)`, `throw(subtype, type, value)` + +Throws an exception that can be caught in a `try` block (see above). If ran without arguments, it will throw a `user_exception` +passing `null` as the value to the `catch_expr`. With two arguments you can mimic any other exception type thrown in scarpet. +With 3 arguments, you can specify a custom exception acting as a `subtype` of a provided `type`, allowing to customize `try` +statements with custom exceptions. + +### `if(cond, expr, cond?, expr?, ..., default?)` + +If statement is a function that takes a number of conditions that are evaluated one after another and if any of +them turns out true, its `expr` gets returned, otherwise, if all conditions fail, the return value is `default` +expression, or `null` if default is skipped + +`if` function is equivalent to `if (cond) expr; else if (cond) expr; else default;` from Java, +just in a functional form diff --git a/docs/scarpet/language/LoopsAndHigherOrderFunctions.md b/docs/scarpet/language/LoopsAndHigherOrderFunctions.md new file mode 100644 index 0000000..34409e6 --- /dev/null +++ b/docs/scarpet/language/LoopsAndHigherOrderFunctions.md @@ -0,0 +1,150 @@ +# Loops, and higher order functions + +Efficient use of these functions can greatly simplify your programs and speed them up, as these functions will +internalize most of the operations that need to be applied on multiple values at the same time. Most of them take +a `list` argument which can be any iterable structure in scarpet, including generators, like `rect`, or `range`, +and maps, where the iterator returns all the map keys + +## Loops + +### `break(), break(expr), continue(), continue(expr)` + +These allow to control execution of a loop either skipping current iteration code, using `continue`, or finishing the +current loop, using `break`. `break` and `continue` can only be used inside `for`, `c_for`, `while`, `loop`, `map`, +`filter`, `reduce` as well as Minecraft API block loops, `scan` and `volume` +functions, while `break` can be used in `first` as well. Outside of the internal expressions of these functions, +calling `break` or `continue` will cause an error. In case of the nested loops, and more complex setups, use +custom `try` and `throw` setup. + +Please check corresponding loop function description what `continue` and `break` do in their contexts, but in +general case, passed values to `break` and `continue` will be used in place of the return value of the internal +iteration expression. + +### `c_for(init, condition, increment, body)` + +`c_for` Mimics c-style tri-arg (plus body) for loops. Return value of `c_for` is number of iterations performed in the + loop. Unlike other loops, the `body` is not provided with pre-initialized `_` style variables - all initialization + and increments has to be handled by the programmers themselves. + `break` and `continue` statements are handled within `body` expression only, and not in `condition` or `increment`. + +
+ c_for(x=0, x<10, x+=1,
+    c_for(y=0, y<10, y+=1,
+        print(str('%d * %d = %d', x, y, x*y))
+    )
+ )
+ 
+ +### `for(list,expr(_,_i))` + +Evaluates expression over list of items from the `list`. Supplies `_`(value) and `_i`(iteration number) to the `expr`. + +Returns the number of times `expr` was successful. Uses `continue` and `break` argument in place of the returned +value from the `expr`(if supplied), to determine if the iteration was successful. + +
+check_prime(n) -> !first( range(2, sqrt(n)+1), !(n % _) );
+for(range(1000000,1100000),check_prime(_))  => 7216
+
+ +From which we can learn that there is 7216 primes between 1M and 1.1M + +### `while(cond, expr)`, `while(cond, limit, expr)` + +Evaluates expression `expr` repeatedly until condition `cond` becomes false, but not more than `limit` times (if limit is specified). +Returns the result of the last `expr` evaluation, or `null` if nothing was successful. Both `expr` and `cond` will +received a bound variable `_` indicating current iteration, so its a number. + +
+while(a<100,a=_*_) => 100 // loop stopped at condition
+while(a<100,10,a=_*_)  => 81 // loop exhausted via limit
+while(a<100,20,a=_*_)  => 100 // loop stopped at condition, but a has already been assigned
+while(_*_<100,20,a=_*_)  => 81 // loop stopped at condition, before a was assigned a value
+
+ +### `loop(num,expr(_),exit(_)?)` + +Evaluates expression `expr`, `num` number of times. code>expr receives `_` system variable indicating the iteration. + +
+loop(5, game_tick())  => repeat tick 5 times
+list = []; loop(5, x = _; loop(5, list += [x, _] ) ); list
+  // double loop, produces: [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [1, 0], [1, 1], ... , [4, 2], [4, 3], [4, 4]]
+
+ +In this small example we will search for first 10 primes, apparently including 0: + +
+check_prime(n) -> !first( range(2, sqrt(n)+1), !(n % _) );
+primes = [];
+loop(10000, if(check_prime(_), primes += _ ; if (length(primes) >= 10, break())));
+primes
+// outputs: [0, 1, 2, 3, 5, 7, 11, 13, 17, 19]
+
+ +## Higher Order Functions + +### `map(list,expr(_,_i))` + +Converts a `list` of values, to another list where each value is result of an expression `v = expr(_, _i)` +where `_` is passed as each element of the list, and `_i` is the index of such element. If `break` is called the +map returns whatever collected thus far. If `continue` and `break` are used with supplied argument, it is used in +place of the resulting map element, otherwise current element is skipped. + +
+map(range(10), _*_)  => [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
+map(player('*'), _+' is stoopid') [gnembon is stoopid, herobrine is stoopid]
+
+ +### `filter(list,expr(_,_i))` + +filters `list` elements returning only these that return positive result of the `expr`. With `break` and `continue` +statements, the supplied value can be used as a boolean check instead. + +
+filter(range(100), !(_%5), _*_>1000)  => [0, 5, 10, 15, 20, 25, 30]
+map(filter(entity_list('*'),_=='Witch'), query(_,'pos') )  => [[1082.5, 57, 1243.5]]
+
+ +### `first(list,expr(_,_i))` + +Finds and returns the first item in the list that satisfies `expr`. It sets `_` for current element value, +and `_i` for index of that element. `break` can be called inside the iteration code, using its argument value +instead of the current item. `continue` has no sense and cannot be called inside `first` call. + +
+first(range(1000,10000), n=_; !first( range(2, sqrt(n)+1), !(n % _) ) )  => 1009 // first prime after 1000
+
+ +Notice in the example above, that we needed to rename the outer `_` to be albe to use in in the inner `first` call + +### `all(list,expr(_,_i))` + +Returns `true` if all elements on the list satisfy the condition. Its roughly equivalent +to `all(list,expr) <=> for(list,expr)==length(list)`. `expr` also receives bound `_` and `_i` variables. `break` +and `continue` have no sense and cannot be used inside of `expr` body. + +
+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), [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
+
+ +### `reduce(list,expr(_a,_,_i), initial)` + +Applies `expr` for each element of the list and saves the result in `_a` accumulator. Consecutive calls to `expr` +can access that value to apply more values. You also need to specify the initial value to apply for the +accumulator. `break` can be used to terminate reduction prematurely. If a value is provided to `break` or `continue`, +it will be used from now on as a new value for the accumulator. + +
+reduce([1,2,3,4],_a+_,0)  => 10
+reduce([1,2,3,4],_a*_,1)  => 24
+
+ diff --git a/docs/scarpet/language/Math.md b/docs/scarpet/language/Math.md new file mode 100644 index 0000000..3bb65ee --- /dev/null +++ b/docs/scarpet/language/Math.md @@ -0,0 +1,121 @@ +# Arithmetic operations + +## Basic Arithmetic Functions + +There is bunch of them - they require a number and spit out a number, doing what you would expect them to do. + +### `fact(n)` + +Factorial of a number, a.k.a `n!`, just not in `scarpet`. Gets big... quick... Therefore, values larger +than `fact(20)` will not return the exact value, but a value with 'double-float' precision. + +### `sqrt(n)` + +Square root (not 'a squirt') of a number. For other fancy roots, use `^`, math and yo noggin. Imagine square roots on a tree... + +### `abs(n)` + +Absolut value. + +### `round(n)` + +Closest integer value. Did you know the earth is also round? + +### `floor(n)` + +Highest integer that is still no larger then `n`. Insert a floor pun here. + +### `ceil(n)` + +First lucky integer that is not smaller than `n`. As you would expect, ceiling is typically right above the floor. + +### `ln(n)` + +Natural logarithm of `n`. Naturally. + +### `ln1p(n)` + +Natural logarithm of `n+1`. Very optimistic. + +### `log10(n)` + +Decimal logarithm of `n`. Its ceiling is the length of its floor. + +### `log(n)` + +Binary logarithm of `n`. Finally, a proper one, not like the previous 11. + +### `log1p(n)` + +Binary logarithm of `n+1`. Also always positive. + +### `mandelbrot(a, b, limit)` + +Computes the value of the mandelbrot set, for set `a` and `b` spot. Spot the beetle. Why not. + +### `min(arg, ...), min(list), max(arg, ...), max(list)` + +Compute minimum or maximum of supplied arguments assuming default sorthoraphical order. +In case you are missing `argmax`, just use `a ~ max(a)`, little less efficient, but still fun. + +Interesting bit - `min` and `max` don't remove variable associations from arguments, which means can be used as +LHS of assignments (obvious case), or argument spec in function definitions (far less obvious). + +
+a = 1; b = 2; min(a,b) = 3; [a,b]  => [3, 2]
+a = 1; b = 2; fun(x, min(a,b)) -> [a,b]; fun(3,5)  => [5, 0]
+
+ +Absolutely no idea, how the latter might be useful in practice. But since it compiles, can ship it. + +### `relu(n)` + +Linear rectifier of `n`. 0 below 0, n above. Why not. `max(0,n)` with less moral repercussions. + +## Trigonometric / Geometric Functions + +### `sin(x)` + +### `cos(x)` + +### `tan(x)` + +### `asin(x)` + +### `acos(x)` + +### `atan(x)` + +### `atan2(x,y)` + +### `sinh(x)` + +### `cosh(x)` + +### `tanh(x)` + +### `sec(x)` + +### `csc(x)` + +### `sech(x)` + +### `csch(x)` + +### `cot(x)` + +### `acot(x)` + +### `coth(x)` + +### `asinh(x)` + +### `acosh(x)` + +### `atanh(x)` + +### `rad(deg)` + +### `deg(rad)` + +Use as you wish diff --git a/docs/scarpet/language/Operators.md b/docs/scarpet/language/Operators.md new file mode 100644 index 0000000..c13921f --- /dev/null +++ b/docs/scarpet/language/Operators.md @@ -0,0 +1,326 @@ +# Operators + +There is a number of operators you can use inside the expressions. Those could be considered generic type operators +that apply to most data types. They also follow standard operator precedence, i.e. `2+2*2` is understood +as `2+(2*2)`, not `(2+2)*2`, otherwise they are applied from left to right, i.e. `2+4-3` is interpreted +as `(2+4)-3`, which in case of numbers doesn't matter, but since `scarpet` allows for mixing all value types +the associativity would matter, and may lead to unintended effects: + +Operators can be unary - with one argument prefixed by the operator (like `-`, `!`, `...`), "practically binary" (that +clearly have left and right operands, like assignment `=` operator), and "technically binary" (all binary operators have left and +right hand, but can be frequently chained together, like `1+2+3`). All "technically binary" operators (where chaining makes sense) +have their functional counterparts, e.g. `1+2+3` is equivalent to `sum(1, 2, 3)`. Functional and operatoral forms are directly +equivalent - they actually will result in the same code as scarpet will optimize long operator chains into their optimized functional forms. + +Important operator is function definition `->` operator. It will be covered +in [User Defined Functions and Program Control Flow](docs/scarpet/language/FunctionsAndControlFlow.md) + +
+'123'+4-2 => ('123'+4)-2 => '1234'-2 => '134'
+'123'+(4-2) => '123'+2 => '1232'
+3*'foo' => 'foofoofoo'
+1357-5 => 1352
+1357-'5' => 137
+3*'foo'-'o' => 'fff'
+[1,3,5]+7 => [8,10,12]
+
+ +As you can see, values can behave differently when mixed with other types in the same expression. +In case values are of the same types, the result tends to be obvious, but `Scarpet` tries to make sense of whatever +it has to deal with + +## Operator Precedence + +Here is the complete list of operators in `scarpet` including control flow operators. Note, that commas and brackets +are not technically operators, but part of the language, even if they look like them: + +* Match, Get `~ :` +* Unary `+ - ! ...` +* Exponent `^` +* Multiplication `* / %` +* Addition `+ -` +* Comparison `> >= <= <` +* Equality `== !=` +* Logical And`&&` +* Logical Or `||` +* Assignment `= += <>` +* Definition `->` +* Next statement`;` +* Comma `,` +* Bracket `( )` + +### `Get, Accessor Operator :` + +Operator version of the `get(...)` function to access elements of lists, maps, and potentially other containers +(i.e. NBTs). It is important to distinguish from `~` operator, which is a matching operator, which is expected to +perform some extra computations to retrieve the result, while `:` should be straightforward and immediate, and +the source object should behave like a container and support full container API, +meaning `get(...)`, `put(...)`, `delete(...)`, and `has(...)` functions + +For certain operators and functions (get, put, delete, has, =, +=) objects can use `:` annotated fields as l-values, +meaning construct like `foo:0 = 5`, would act like `put(foo, 0, 5)`, rather than `get(foo, 0) = 5`, +which would result in an error. + +TODO: add more information about l-value behaviour. + +### `Matching Operator ~` + +This operator should be understood as 'matches', 'contains', 'is_in', or 'find me some stuff about something else. +For strings it matches the right operand as a regular expression to the left, returning: + - `null` if there is no match + - matched phrase if no grouping is applied + - matched element if one group is applied + - list of matches if more than one grouping is applied + +This can be used to extract information from unparsed nbt's in a more convoluted way (use `get(...)` for +more appropriate way of doing it). For lists it checks if an element is in the list, and returns the +index of that element, or `null` if no such element was found, especially that the use of `first(...)` +function will not return the index. Currently it doesn't have any special behaviour for numbers - it checks for +existence of characters in string representation of the left operand with respect of the regular expression on +the right hand side. + +In Minecraft API portion `entity ~ feature` is a shortcode for `query(entity,feature)` for queries that do not take +any extra arguments. + +
+[1,2,3] ~ 2  => 1
+[1,2,3] ~ 4  => null
+
+'foobar' ~ 'baz'  => null
+'foobar' ~ '.b'  => 'ob'
+'foobar' ~ '(.)b'  => 'o'
+'foobar' ~ '((.)b)'  => ['ob', 'o']
+'foobar' ~ '((.)(b))'  => ['ob', 'o', 'b']
+'foobar' ~ '(?:(.)(?:b))'  => 'o'
+
+player('*') ~ 'gnembon'  // null unless player gnembon is logged in (better to use player('gnembon') instead
+p ~ 'sneaking' // if p is an entity returns whether p is sneaking
+
+ +Or a longer example of an ineffective way to searching for a squid + +
+entities = entities_area('all',x,y,z,100,10,100);
+sid = entities ~ 'Squid';
+if(sid != null, run('execute as '+query(get(entities,sid),'id')+' run say I am here '+query(get(entities,sid),'pos') ) )
+
+ +Or an example to find if a player has specific enchantment on a held axe (either hand) and get its level +(not using proper NBTs query support via `get(...)`): + +
+global_get_enchantment(p, ench) -> (
+$   for(['mainhand','offhand'],
+$      holds = query(p, 'holds', _);
+$      if( holds,
+$         [what, count, nbt] = holds;
+$         if( what ~ '_axe' && nbt ~ ench,
+$            lvl = max(lvl, number(nbt ~ '(?<=lvl:)\\d') )
+$         )
+$      )
+$   );
+$   lvl
+$);
+/script run global_get_enchantment(player(), 'sharpness')
+
+ +### Basic Arithmetic Operators `+`, `sum(...)`, `-`, `difference(...)`, `*`, `product(...)`, `/`, `quotient(...)` + +Allows to add the results of two expressions. If the operands resolve to numbers, the result is arithmetic operation. +In case of strings, adding or subtracting from a string results in string concatenation and removal of substrings +from that string. Multiplication of strings and numbers results in repeating the string N times and division results +in taking the first k'th part of the string, so that `str*n/n ~ str` + +In case first operand is a list, either it +results in a new list with all elements modified one by one with the other operand, or if the operand is a list +with the same number of items - element-wise addition/subtraction. This prioritize treating lists as value containers +to lists treated as vectors. + +Addition with maps (`{}` or `m()`) results in a new map with keys from both maps added, if both operands are maps, +adding elements of the right argument to the keys, of left map, or just adding the right value as a new key +in the output map. + +Functional forms of `-` and `/` have less intuitive multi-nary interpretation, but they might be useful in some situations. +`x-y-z` resolves to `difference(x, y, z)`. + +`/` always produces a properly accurate result, fully reversible with `*` operator. To obtain a integer 'div' result, use +`floor(x/y)`. + +Examples: + +
+2+3 => 5
+'foo'+3+2 => 'foo32'
+'foo'+(3+2) => 'foo5'
+3+2+'bar' => '5bar'
+'foo'*3 => 'foofoofoo'
+'foofoofoo' / 3 => 'foo'
+'foofoofoo'-'o' => 'fff'
+[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}
+
+ +### Just Operators `%`, `^` + +The modulo and exponent (power) operators work only if both operands are numbers. `%` is a proper (and useful) 'modulus' operator, +not a useless 'reminder' operator that you would expect from anything that touches Java. While typically modulus is reserved +to integer numbers, scarpet expands them to floats with as much sense as possible. + +
pi^pi%euler  => 1.124....
+-9 % 4  => 3
+9 % -4  => -3
+9.1 % -4.2  => -3.5
+9.1 % 4.2  => 0.7
+-3 ^ 2  => 9
+-3 ^ pi => // Error
+
+ +### Comparison Operators `==`, `equal()`, `!=`, `unique()`, `<`, `increasing()`, `>`, `decreasing()`, `<=`, `nondecreasing()`, `>=`, `nonincreasing()` + +Allows to compare the results of two expressions. For numbers, it considers arithmetic order of numbers, for +strings - lexicographical, nulls are always 'less' than everything else, and lists check their elements - +if the sizes are different, the size matters, otherwise, pairwise comparisons for each element are performed. +The same order rules than with all these operators are used with the default sortographical order as used by `sort` +function. All of these are true: + +
+null == null
+null != false
+0 == false
+1 == true
+null < 0
+null < -1000
+1000 < 'a'
+'bar' < 'foo'
+3 == 3.0
+
+ +Functional variants of these operators allow to assert certain paradigms on multiple arguments at once. This means that +due to formal equivalence `x < y < z` is equivalent to `x < y & y < z` because of direct mapping to `increasing(x, y, z)`. This translates through +the parentheses, so `((x < y) < z)` is the same as `increasing(x, y, z)`. To achieve the same effect as you would see in other + languages (not python), you would need to cast the first pair to boolean value, i.e. `bool(x < y) < z`. + +### Logical Operators `&&`, `and(...)`, `||`, `or(...)` + +These operator compute respective boolean operation on the operands. What it important is that if calculating of the +second operand is not necessary, it won't be evaluated, which means one can use them as conditional statements. In +case of success returns first positive operand (`||`) or last one (`&&`). + +
+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
+
+ +### `Assignment Operators = <> +=` + +A set of assignment operators. All require bounded variable on the LHS, `<>` requires bounded arguments on the +right hand side as well (bounded, meaning being variables). Additionally they can also handle list constructors +with all bounded variables, and work then as list assignment operators. When `+=` is used on a list, it extends +that list of that element, and returns the list (old == new). `scarpet` doesn't support currently removal of items. +Removal of items can be obtained via `filter` command, and reassigning it fo the same variable. Both operations would +require rewriting of the array anyways. + +
+a = 5  => a == 5
+[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]
+
+ +### `Unary Operators - +` + +Require a number, flips the sign. One way to assert it's a number is by crashing the script. gg. + +
+-4  => -4
++4  => 4
++'4'  // Error message
+
+ +### `Negation Operator !` + +flips boolean condition of the expression. Equivalent of `bool(expr)==false` + +
+!true  => false
+!false  => true
+!null  => true
+!5  => false
+![] => true
+![null] => false
+
+ +### `Unpacking Operator ...` + +Unpacks elements of a list of an iterator into a sequence of arguments in a function making so that `fun(...[1, 2, 3])` is +identical with `fun(1, 2, 3)`. For maps, it unpacks them to a list of key-value pairs. + +In function signatures it identifies a vararg parameter. + +
+fun(a, b, ... rest) -> [a, b, rest]; fun(1, 2, 3, 4)    => [1, 2, [3, 4]]
+
+ +Effects of `...` can be surprisingly lasting. It is kept through the use of variables and function calls. + +
+fun(a, b, ... rest) -> [a, b, ... rest]; fun(1, 2, 3, 4)    => [1, 2, 3, 4]
+args() -> ... [1, 2, 3]; sum(a, b, c) -> a+b+c; sum(args())   => 6
+a = ... [1, 2, 3]; sum(a, b, c) -> a+b+c; sum(a)   => 6
+
+ +Unpacking mechanics can be used for list and map construction, not just for function calls. + +
+[...range(5), pi, ...range(5,-1,-1)]   => [0, 1, 2, 3, 4, 3.14159265359, 5, 4, 3, 2, 1, 0]
+{ ... map(range(5),  _  -> _*_ )}   => {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
+{...{1 -> 2, 3 -> 4}, ...{5 -> 6, 7 -> 8}}   => {1: 2, 3: 4, 5: 6, 7: 8}
+
+ +Fine print: unpacking of argument lists happens just before functions are evaluated. +This means that in some situations, for instance +when an expression is expected (`map(list, expr)`), or a function should not evaluate some (most!) of its arguments (`if(...)`), +unpacking cannot be used, and will be ignored, leaving `... list` identical to `list`. +Functions that don't honor unpacking mechanics, should have no use for it at the first place + (i.e. have one, or very well-defined, and very specific parameters), +so some caution (prior testing) is advised. Some of these multi-argument built-in functions are + `if`, `try`, `sort_key`, `system_variable_get`, `synchronize`, `sleep`, `in_dimension`, +all container functions (`get`, `has`, `put`, `delete`), +and all loop functions (`while`, `loop`, `map`, `filter`, `first`, `all`, `c_for`, `for` and`reduce`). + +### `Binary (bitwise) operations` + +These are a bunch of operators that work exclusively on numbers, more specifically their binary representations. Some of these +work on multiple numbers, some on only 2, and others on only 1. Note that most of these functions (all but `double_to_long_bits`) +only take integer values, so if the input has a decimal part, it will be discarded. + + - `bitwise_and(...)` -> Does the bitwise AND operation on each number in order. Note that with larger ranges of numbers this will + tend to 0. + - `bitwise_xor(...)` -> Does the bitwise XOR operation on each number in order. + - `bitwise_or(...)` -> Does the bitwise AND operation on each number in order. Note that with larger ranges of numbers this will + tend to -1. + - `bitwise_shift_left(num, amount)` -> Shifts all the bits of the first number `amount` spots to the left. Note that only the 6 + lowest-order bits of the amount are considered. + - `bitwise_shift_right(num, amount)` -> Shifts all the bits of the first number `amount` spots to the right logically. That is, the + `amount` most significant bits will always be set to 0. Like with the above, only the 6 lowest-order bits of the amount are considered. + - `bitwise_arithmetic_shift_right(num, amount)` -> Shifts all the bits of the first number `amount` spots to the right arithmetically. + That is, if the most significant (sign) bit is a 1, it'll propagate the one to the `amount` most significant bits. Like with the above, + only the 6 lowest-order bits of the amount are considered. + - `bitwise_roll_left(num, amount)` -> Rolls the bits of the first number `amount` bits to the left. This is basically where you + shift out the first `amount` bits and then add them on at the back, essentially 'rolling' the number. Note that unlike with + shifting, you can roll more than 63 bits at a time, as it just makes the number roll over more times, which isn't an issue + - `bitwise_roll_right(num, amount)` -> Same as above, just rolling in the other direction + - `bitwise_not(num)` -> Flips all the bits of the number. This is simply done by performing xor operation with -1, which in binary is + all ones. + - `bitwise_popcount(num)` -> Returns the number of ones in the binary representation of the number. For the number of zeroes, just + do 64 minus this number. + - `double_to_long_bits(num)` -> Returns a representation of the specified floating-point value according to the IEEE 754 floating-point + "double format" bit layout. + - `long_to_double_bits(num)` -> Returns the double value corresponding to a given bit representation. diff --git a/docs/scarpet/language/Overview.md b/docs/scarpet/language/Overview.md new file mode 100644 index 0000000..80da2ba --- /dev/null +++ b/docs/scarpet/language/Overview.md @@ -0,0 +1,293 @@ +# Fundamental components of `scarpet` programming language. + +Scarpet (a.k.a. Carpet Script, or Script for Carpet) is a programming language +designed to provide the ability to write custom programs to run within Minecraft +and interact with the world. + +This specification is divided into two sections: this one is agnostic to any +Minecraft related features and could function on its own, and CarpetExpression +for Minecraft specific routines and world manipulation functions. + +# Synopsis + +
+script run print('Hello World!')
+
+ +or an OVERLY complex example: + +
+/script run
+    block_check(x1, y1, z1, x2, y2, z2, block_to_check) ->
+    (
+        [minx, maxx] = sort([x1, x2]);
+        [miny, maxy] = sort([y1, y2]);
+        [minz, maxz] = sort([z1, z2]);
+        'Need to compute the size of the area of course';
+        'Cause this language doesn\'t support comments in the command mode';
+        xsize = maxx - minx + 1;
+        ysize = maxy - miny + 1;
+        zsize = maxz - minz + 1;
+        total_count = 0;
+        loop(xsize,
+            xx = minx + _ ;
+            loop(ysize,
+                yy = miny + _ ;
+                loop(zsize,
+                    zz = minz + _ ;
+                    if ( block(xx,yy,zz) == block_to_check,
+                        total_count += ceil(rand(1))
+                    )
+                )
+            )
+        );
+        total_count
+    );
+    check_area_around_closest_player_for_block(block_to_check) ->
+    (
+        closest_player = player();
+        [posx, posy, posz] = query(closest_player, 'pos');
+        total_count = block_check( posx-8,1,posz-8, posx+8,17,posz+8, block_to_check);
+        print('There is '+total_count+' of '+block_to_check+' around you')
+    )
+/script invoke check_area_around_closest_player_for_block 'diamond_ore'
+
+ +or simply + +
+/script run print('There is '+for(rect(x,9,z,8,8,8), _ == 'diamond_ore')+' diamond ore around you')
+
+ +It definitely pays to check what higher level `scarpet` functions have to offer. + +# Programs + +You can think of an program like a mathematical expression, like `"2.4*sin(45)/(2-4)"` or `"sin(y)>0 & max(z, 3)>3"`. +Writing a program, is like writing a `2+3`, just a bit longer. + +## Basic language components + +Programs consist of constants, like `2`, `3.14`, `pi`, or `'foo'`, operators like `+`, `/`, `->`, variables which you +can define, like `foo` or special ones that will be defined for you, like `_x`, or `_` , which are specific to each +built in function, and functions with name, and arguments in the form of `f(a,b,c)`, where `f` is the function name, +and `a, b, c` are the arguments which can be any other expression. And that's all the parts of the language, so all +in all - sounds quite simple. + +## Code flow + +Like any other proper programming language, `scarpet` needs brackets, basically to identify where stuff begins and +where it ends. In the languages that uses much more complicated constructs, like Java, they tend to use all sort of +them, round ones to indicate function calls, curly to indicate section of code, square to access lists, pointy for +generic types etc... I mean - there is no etc, cause they have exhausted all the bracket options... + +`Scarpet` is different, since it runs everything based on functions (although its not per se a functional +language like lisp) only needs the round brackets for everything, and it is up to the programmer to organize +its code so its readable, as adding more brackets does not have any effect on the performance of the programs +as they are compiled before they are executed. Look at the following example usage of `if()` function: + +
+if(x<y+6,set(x,8+y,z,'air');plop(x,top('surface',x,z),z,'birch'),sin(query(player(),'yaw'))>0.5,plop(0,0,0,'boulder'),particle('fire',x,y,z))
+
+ +Would you prefer to read + +
+if(   x<y+6,
+           set(x,8+y,z,'air');
+           plop(x,top('surface',x,z),z,'birch'),
+      sin(query(player(),'yaw'))>0.5,
+           plop(0,0,0,'boulder'),
+      particle('fire',x,y,z)
+)
+
+ +Or rather: + +
+if
+(   x<y+6,
+    (
+        set(x,8+y,z,'air');
+        plop(x,top('surface',x,z),z,'birch')
+    ),
+    // else if
+    sin(query(player(),'yaw'))>0.5,
+    (
+        plop(0,0,0,'boulder')
+    ),
+    // else
+    particle('fire',x,y,z)
+)
+
+ +Whichever style you prefer it doesn't matter. It typically depends on the situation and the complexity of the +subcomponents. No matter how many whitespaces and extra brackets you add - the code will evaluate to exactly the +same expression, and will run exactly the same, so make sure your programs are nice and clean so others don't +have problems with them + +## Functions and scoping + +Users can define functions in the form `fun(args....) -> expression` and they are compiled and saved for further +execution in this, but also subsequent calls of /script command, added to events, etc. Functions can also be + assigned to variables, +passed as arguments, called with `call('fun', args...)` function, but in most cases you would want to +call them directly by +name, in the form of `fun(args...)`. This means that once defined functions are saved with the world for +further use. For variables, there are two types of them, global - which are shared anywhere in the code, +and those are all which name starts with 'global_', and local variables which is everything else and those +are only visible inside each function. This also means that all the parameters in functions are +passed 'by value', not 'by reference'. + +## Outer variables + +Functions can still 'borrow' variables from the outer scope, by adding them to the function signature wrapped +around built-in function `outer`. It adds the specified value to the function call stack so they behave exactly +like capturing lambdas in Java, but unlike java captured variables don't need to be final. Scarpet will just +attach their new values at the time of the function definition, even if they change later. Most value will be +copied, but mutable values, like maps or lists, allow to keep the 'state' with the function, allowing them to +have memory and act like objects so to speak. Check `outer(var)` for details. + +## Code delivery, line indicators + +Note that this should only apply to pasting your code to execute with commandblock. Scarpet recommends placing +your code in apps (files with `.sc` extension that can be placed inside `/scripts` folder in the world files +or as a globally available app in singleplayer in the `.minecraft/config/carpet/scripts` folder and loaded +as a Scarpet app with the command `/script load [app_name]`. Scarpet apps loaded from disk should only +contain code, no need to start with `/script run` prefix. + +The following is the code that could be provided in a `foo.sc` app file located in world `/scripts` folder + +
+run_program() -> (
+  loop( 10,
+    // looping 10 times
+    // comments are allowed in scripts located in world files
+    // since we can tell where that line ends
+    foo = floor(rand(10));
+    check_not_zero(foo);
+    print(_+' - foo: '+foo);
+    print('  reciprocal: '+  _/foo )
+  )
+);
+check_not_zero(foo) -> (
+  if (foo==0, foo = 1)
+)
+
+ +Which we then call in-game with: + +
+/script load foo
+/script in foo invoke run_program
+
+ +However the following code can also be input as a command, or in a command block. + +Since the maximum command that can be input to the chat is limited in length, you will be probably inserting your +programs by pasting them to command blocks or reading from world files, however pasting to command blocks will +remove some whitespaces and squish your newlines making the code not readable. If you are pasting a program that +is perfect and will never cause an error, I salute you, but for the most part it is quite likely that your program +might break, either at compile time, when its initially analyzed, or at execute time, when you suddenly attempt to +divide something by zero. In these cases you would want to get a meaningful error message, but for that you would +need to indicate for the compiler where did you put these new lines, since command block would squish them. For that, +place at the beginning of the line to let the compiler know where are you. This makes so that `$` is the only +character that is illegal in programs, since it will be replaced with new lines. As far as I know, `$` is not +used anywhere inside Minecraft identifiers, so this shouldn't hinder the abilities of your programs. + +Consider the following program executed as command block command: + +
+/script run
+run_program() -> (
+  loop( 10,
+    foo = floor(rand(_));
+    check_not_zero(foo);
+    print(_+' - foo: '+foo);
+    print('  reciprocal: '+  _/foo )
+  )
+);
+check_not_zero(foo) -> (
+   if (foo==0, foo = 1)
+)
+
+ +Lets say that the intention was to check if the bar is zero and prevent division by zero in print, but because +the `foo` is passed as a variable, it never changes the original foo value. Because of the inevitable division +by zero, we get the following message: + +
+Your math is wrong, Incorrect number format for NaN at pos 98
+run_program() -> ( loop( 10, foo = floor(rand(_)); check_not_zero(foo); print(_+' - foo: '+foo);
+HERE>> print(' reciprocal: '+ _/foo ) ));check_not_zero(foo) -> ( if (foo==0, foo = 1))
+
+ +As we can see, we got our problem where the result of the mathematical operation was not a number +(infinity, so not a number), however by pasting our program into the command made it squish the newlines so +while it is clear where the error happened and we still can track the error down, the position of the error (98) +is not very helpful and wouldn't be useful if the program gets significantly longer. To combat this issue we can +precede every line of the script with dollar signs `$`: + +
+/script run
+$run_program() -> (
+$  loop( 10,
+$    foo = floor(rand(_));
+$    check_not_zero(foo);
+$    print(_+' - foo: '+foo);
+$    print('  reciprocal: '+  _/foo )
+$  )
+$);
+$check_not_zero(foo) -> (
+$   if (foo==0, foo = 1)
+$)
+
+ +Then we get the following error message + +
+Your math is wrong, Incorrect number format for NaN at line 7, pos 2
+  print(_+' - foo: '+foo);
+   HERE>> print(' reciprocal: '+ _/foo )
+  )
+
+ +As we can note not only we get much more concise snippet, but also information about the line number and position, +so means its way easier to locate the potential problems problem + +Obviously that's not the way we intended this program to work. To get it `foo` modified via a function call, +we would either return it as a result and assign it to the new variable: + +
+foo = check_not_zero(foo);
+...
+check_not_zero(foo) -> if(foo == 0, 1, foo)
+
+ +.. or convert it to a global variable, which in this case passing as an argument is not required + +
+global_foo = floor(rand(10));
+check_foo_not_zero();
+...
+check_foo_not_zero() -> if(global_foo == 0, global_foo = 1)
+
+ +## Scarpet preprocessor + +There are several preprocessing operations applied to the source of your program to clean it up and prepare for +execution. Some of them will affect your code as it is reported via stack traces and function definition, and some +are applied only on the surface. + - stripping `//` comments (in file mode) + - replacing `$` with newlines (in command mode, modifies submitted code) + - removing extra semicolons that don't follow `;` use as a binary operator, allowing for lenient use of semicolons + - translating `{` into `m(`, `[` into `l(`, and `]` and `}` into `)` + +No further optimizations are currently applied to your code. + +## Mentions + +LR1 parser, tokenizer, and several built-in functions are built based on the EvalEx project. +EvalEx is a handy expression evaluator for Java, that allows to evaluate +simple mathematical and boolean expressions. EvalEx is distributed under MIT licence. +For more information, see: [EvalEx GitHub repository](https://github.com/uklimaschewski/EvalEx) diff --git a/docs/scarpet/language/SystemFunctions.md b/docs/scarpet/language/SystemFunctions.md new file mode 100644 index 0000000..924dbce --- /dev/null +++ b/docs/scarpet/language/SystemFunctions.md @@ -0,0 +1,430 @@ +# System functions + +## Type conversion functions + +### `copy(expr)` + +Returns the deep copy of the expression. Can be used to copy mutable objects, like maps and lists + +### `type(expr)` + +Returns the string value indicating type of the expression. Possible outcomes +are `null`, `number`, `string`, `list`, `map`, `iterator`, `function`, `task`, +as well as minecraft related concepts like `block`, `entity`, `nbt`, `text`. + +### `bool(expr)` + +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. + +
+bool(pi) => true
+bool(false) => false
+bool('') => false
+bool([]) => false
+bool(['']) => true
+bool('foo') => true
+bool('false') => false
+bool('nulL') => false
+if('false',1,0) => true
+
+ +### `number(expr)` + +Returns a numeric context of the expression. Can be used to read numbers from strings, or other types + +
+number(null) => 0
+number(false) => 0
+number(true) => 1
+number('') => null
+number('3.14') => 3.14
+number([]) => 0
+number(['']) => 1
+number('foo') => null
+number('3bar') => null
+number('2')+number('2') => 4
+
+ +### `str(expr)`,`str(expr, params? ... )`, `str(expr, param_list)` + +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', [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'
+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'
+
+ +* * * +## Threading and Parallel Execution + +Scarpet allows to run threads of execution in parallel to the main script execution thread. In Minecraft, apps +are executed on the main server thread. Since Minecraft is inherently NOT thread safe, it is not that +beneficial to parallel execution in order to access world resources faster. Both `getBlockState` and `setBlockState` +are not thread safe and require the execution to park on the server thread, where these requests can be executed in +the off-tick time in between ticks that didn't take all 50ms. There are however benefits of running things in parallel, +like fine time control not relying on the tick clock, or running things independent on each other. You can still run +your actions on tick-by-tick basis, either taking control of the execution using `game_tick()` API function +(nasty solution), or scheduling tick using `schedule()` function (preferred solution), but threading gives much more control +on the timings without impacting the main game and is the only solution to solve problems in parallel +(see [scarpet camera](/src/main/resources/assets/carpet/scripts/camera.sc)). + +Due to limitations with the game, there are some limits to the threading as well. You cannot for +instance `join_task()` at all from the main script and server thread, because any use of Minecraft specific +function that require any world access, will require to park and join on the main thread to get world access, +meaning that calling join on that task would inevitably lead to a typical deadlock. You can still join tasks +from other threads, just because the only possibility of a deadlock in this case would come explicitly from your +bad code, not the internal world access behaviour. Some things tough like players or entities manipulation, can be +effectively parallelized. + +If the app is shutting down, creating new tasks via `task` will not succeed. Instead the new task will do nothing and return +`null`, so most threaded application should handle closing apps naturally. Keep in mind in case you rely on task return values, +that they will return `null` regardless of anything in these situations. When app handles `__on_close()` event, new tasks cannot +be submitted at this point, but current tasks are not terminated. Apps can use that opportunity to gracefully shutdown their tasks. +Regardless if the app handles `__on_close()` event, or does anything with their tasks in it, all tasks will be terminated exceptionally +within the next 1.5 seconds. + +### `task(function, ... args)`, `task_thread(executor, function, ... args)` + +Creates and runs a parallel task, returning the handle to the task object. Task will return the return value of the +function when its completed, or will return `null` immediately if task is still in progress, so grabbing a value of +a task object is non-blocking. Function can be either function value, or function lambda, or a name of an existing +defined function. In case function needs arguments to be called with, they should be supplied after the function +name, or value. `executor` identifier in `task_thread`, places the task in a specific queue identified by this value. +The default thread value is the `null` thread. There are no limits on number of parallel tasks for any executor, +so using different queues is solely for synchronization purposes. + +
+task( _() -> print('Hello Other World') )  => Runs print command on a separate thread
+foo(a, b) -> print(a+b); task('foo',2,2)  => Uses existing function definition to start a task
+task_thread('temp', 'foo',3,5);  => runs function foo with a different thread executor, identified as 'temp'
+a = 3; task_thread('temp', _(outer(a), b) -> foo(a,b), 5)  
+    => Another example of running the same thing passing arguments using closure over anonymous function as well as passing a parameter.
+
+ +In case you want to create a task based on a function that is not defined in your module, please read the tips on + "Passing function references to other modules of your application" section in the `call(...)` section. + +### `sleep()` `sleep(timeout)`, `sleep(timeout, close_expr)` + + +Halts the execution of the thread (or the game itself, if run not as a part of a task) for `expr` milliseconds. +It checks for interrupted execution, in that case exits the thread (or the entire program, if not run on a thread) in case the app +is being stopped/removed. If the closing expression is specified, executes the expression when a shutdown signal is triggered. +If run on the main thread (i.e. not as a task) the close expression may only be invoked when the entire game shuts down, so close call only +makes sense for threads. For regular programs, use `__on_close()` handler. + +Since `close_expr` is executed after app shutdown is initiated, you won't be able to create new tasks in that block. Threads +should periodically call `sleep` to ensure all app tasks will finish when the app is closing or right after, but the app engine +will not forcefully remove your running tasks, so the tasks themselves need to properly react to the closing request. + +
+sleep(50)  # wait for 50 milliseconds
+sleep(1000, print('Interrupted')) # waits for 1 second, outputs a message when thread is shut down.
+
+ +### `task_count(executor?)` + +If no argument provided, returns total number of tasks being executed in parallel at this moment using scarpet +threading system. If the executor is provided, returns number of active tasks for that provider. Use `task_count(null)` +to get the task count of the default executor only. + +### `task_value(task)` + +Returns the task return value, or `null` if task hasn't finished yet. Its a non-blocking operation. Unlike `join_task`, +can be called on any task at any point + +### `task_join(task)` + +Waits for the task completion and returns its computed value. If the task has already finished returns it immediately. +Unless taking the task value directly, i.e. via `task_value`, this operation is blocking. Since Minecraft has a +limitation that all world access operations have to be performed on the main game thread in the off-tick time, +joining any tasks that use Minecraft API from the main thread would mean automatic lock, so joining from the main +thread is not allowed. Join tasks from other threads, if you really need to, or communicate asynchronously with +the task via globals or function data / arguments to monitor its progress, communicate, get partial results, +or signal termination. + +### `task_completed(task)` + +Returns true if task has completed, or false otherwise. + +### `synchronize(lock, expression)` + +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 + +### `lower(expr), upper(expr), title(expr)` + +Returns lowercase, uppercase or titlecase representation of a string representation of the passed expression + +
+lower('aBc') => 'abc'
+upper('aBc') => 'ABC'
+title('aBc') => 'Abc'
+
+ +### `replace(string, regex, repl?); replace_first(string, regex, repl?)` + +Replaces all, or first occurrence of a regular expression in the string with `repl` expression, +or nothing, if not specified. To use escape characters (`\(`,`\+`,...), metacharacters (`\d`,`\w`,...), or position anchors (`\b`,`\z`,...) in your regular expression, use two backslashes. + +
+replace('abbccddebfg','b+','z')  // => azccddezfg
+replace('abbccddebfg','\\w$','z')  // => abbccddebfz
+replace_first('abbccddebfg','b+','z')  // => azccddebfg
+
+ +### `length(expr)` + +Returns length of the expression, the length of the string, the length of the integer part of the number, +or length of the list + +
+length(pi) => 1
+length(pi*pi) => 1
+length(pi^pi) => 2
+length([]) => 0
+length([1,2,3]) => 3
+length('') => 0
+length('foo') => 3
+
+ +### `rand(expr), rand(expr, seed)` + +returns a random number from `0.0` (inclusive) to `expr` (exclusive). In boolean context (in conditions, +boolean functions, or `bool`), returns false if the randomly selected value is less than 1. This means +that `bool(rand(2))` returns true half of the time and `!rand(5)` returns true for 20% (1/5) of the time. If seed is not +provided, uses a random seed that's shared across all scarpet apps. +If seed is provided, each consecutive call to rand() will act like 'next' call to the +same random object. Scarpet keeps track of up to 65536 custom random number generators (custom seeds, per app), +so if you exceed this number, your random sequences will revert to the beginning and start over. + +
+map(range(10), floor(rand(10))) => [5, 8, 0, 6, 9, 3, 9, 9, 1, 8]
+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]
+
+ +## `reset_seed(seed)` + +Resets the sequence of the randomizer used by `rand` for this seed to its initial state. Returns a boolean value +indicating if the given seed has been used or not. + +### `perlin(x), perlin(x, y), perlin(x, y, z), perlin(x, y, z, seed)` + +returns a noise value from `0.0` to `1.0` (roughly) for 1, 2 or 3 dimensional coordinate. The default seed it samples +from is `0`, but seed can be specified as a 4th argument as well. In case you need 1D or 2D noise values with custom +seed, use `null` for `y` and `z`, or `z` arguments respectively. + +Perlin noise is based on a square grid and generates rougher maps comparing to Simplex, which is creamier. +Querying for lower-dimensional result, rather than affixing unused dimensions to constants has a speed benefit, + +Thou shall not sample from noise changing seed frequently. Scarpet will keep track of the last 256 perlin seeds +used for sampling providing similar speed comparing to the default seed of `0`. In case the app engine uses more +than 256 seeds at the same time, switching between them can get much more expensive. + +### `simplex(x, y), simplex(x, y, z), simplex(x, y, z, seed)` + +returns a noise value from `0.0` to `1.0` (roughly) for 2 or 3 dimensional coordinate. The default seed it samples +from is `0`, but seed can be specified as a 4th argument as well. In case you need 2D noise values with custom seed, +use `null` for `z` argument. + +Simplex noise is based on a triangular grid and generates smoother maps comparing to Perlin. To sample 1D simplex +noise, affix other coordinate to a constant. + +Thou shall not sample from noise changing seed frequently. Scarpet will keep track of the last 256 simplex seeds +used for sampling providing similar speed comparing to the default seed of `0`. In case the app engine uses more +than 256 seeds at the same time, switching between them can get much more expensive. + +### `print(expr)`, `print(player, expr)` + +prints the value of the expression to chat. Passes the result of the argument to the output unchanged, +so `print`-statements can be weaved in code to debug programming issues. By default it uses the same communication +channels that most vanilla commands are using. + +In case player is directly specified, it only sends the message to that player, like `tell` command. + +
+print('foo') => results in foo, prints: foo
+a = 1; print(a = 5) => results in 5, prints: 5
+a = 1; print(a) = 5 => results in 5, prints: 1
+print('pi = '+pi) => prints: pi = 3.141592653589793
+print(str('pi = %.2f',pi)) => prints: pi = 3.14
+
+ +### `time()` + +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();
+flip_my_world_upside_down();
+print(str('this took %d milliseconds',time()-start_time))
+
+ +### `unix_time()` + +Returns standard POSIX time as a number of milliseconds since the start of the epoch +(00:00 am and 0 seconds, 1 Jan 1970). +Unlike the previous function, this can be used to get exact time, but it varies from time zone to time zone. + +### `convert_date(milliseconds)` +### `convert_date(year, month, date, hours?, mins?, secs?)` +### `convert_date([year, month, date, hours?, mins?, secs?])` + +If called with a single argument, converts standard POSIX time to a list in the format: + +`[year, month, date, hours, mins, secs, day_of_week, day_of_year, week_of_year]` + +eg: `convert_date(1592401346960) -> [2020, 6, 17, 10, 42, 26, 3, 169, 25]` + +Where the `6` stands for June, but `17` stands for 17th, `10` stands for 10am, +`42` stands for 42 minutes past the hour, and `26` stands for 26 seconds past the minute, +and `3` stands for Wednesday, `169` is the day of year, and `25` is a week of year. + +Run `convert_date(unix_time())` to get current time as list. + + +When called with a list, or with 3 or 6 arguments, returns standard POSIX time as a number of milliseconds since the + start of the epoch (1 Jan 1970), +using the time inputted into the function as opposed to the system time. + +Example editing: +
+date = convert_date(unix_time());
+
+months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+
+days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
+
+print(
+  str('Its %s, %d %s %d, %02d:%02d:%02d', 
+    days:(date:6-1), date:2, months:(date:1-1), date:0, date:3, date:4, date:5 
+  )
+)  
+
+ +This will give you a date: + +It is currently `hrs`:`mins` and `secs` seconds on the `date`th of `month`, `year` + +### `encode_b64(string)`, `decode_b64(string)` + +Encode or decode a string from b64, throwing a `b64_error` exception if it's invalid + +### `encode_json(value)`, `decode_json(string)` + +Encodes a value as a json string, and decodes a json string as a valid value, throwing a `json_error` exception if it +doesn't parse properly + +### `profile_expr(expression)` + +Returns number of times given expression can be run in 50ms time. Useful to profile and optimize your code. +Note that, even if its only a number, it WILL run these commands, so if they are destructive, you need to be careful. + +* * * + +## Access to variables and stored functions (use with caution) + +### `var(expr)` + +Returns the variable under the name of the string value of the expression. Allows to manipulate variables in more +programmatic manner, which allows to use local variable set with a hash map type key-value access, +can also be used with global variables + +
+a = 1; var('a') = 'foo'; a => a == 'foo'
+
+ +### `undef(expr)` + +Removes all bindings of a variable with a name of `expr`. Removes also all function definitions with that name. +It can affect global variable pool, and local variable set for a particular function. + +
+inc(i) -> i+1; foo = 5; inc(foo) => 6
+inc(i) -> i+1; foo = 5; undef('foo'); inc(foo) => 1
+inc(i) -> i+1; foo = 5; undef('inc'); undef('foo'); inc(foo) => Error: Function inc is not defined yet at pos 53
+
+ +### `vars(prefix)` + +It returns all names of variables from local scope (if prefix does not start with 'global') or global variables +(otherwise). Here is a larger example that uses combination of `vars` and `var` functions to be +used for object counting + +
+/script run
+$ count_blocks(ent) -> (
+$   [cx, cy, cz] = query(ent, 'pos');
+$   scan(cx, cy, cz, 16, 16, 16, var('count_'+_) += 1);
+$   for ( sort_key( vars('count_'), -var(_)),
+$     print(str( '%s: %d', slice(_,6), var(_) ))
+$   )
+$ )
+/script run count_blocks(player())
+
+ +* * * + +## System key-value storage + +Scarpet runs apps in isolation. The can share code via use of shared libraries, but each library that is imported to +each app is specific to that app. Apps can store and fetch state from disk, but its restricted to specific locations +meaning apps cannot interact via disk either. To facilitate communication for interappperability, scarpet hosts its +own key-value storage that is shared between all apps currently running on the host, providing methods for getting an +associated value with optional setting it if not present, and an operation of modifying a content of a system +global value. + +### `system_variable_get(key, default_value ?)` + +Returns the variable from the system shared key-value storage keyed with a `key` value, optionally if value is +not present, and default expression is provided, sets a new value to be associated with that key + +### `system_variable_set(key, new_value)` + +Returns the variable from the system shared key-value storage keyed with a `key` value, and sets a new +mapping for the key diff --git a/docs/scarpet/language/VariablesAndConstants.md b/docs/scarpet/language/VariablesAndConstants.md new file mode 100644 index 0000000..a7657f4 --- /dev/null +++ b/docs/scarpet/language/VariablesAndConstants.md @@ -0,0 +1,34 @@ +# Variables and Constants + +`scarpet` provides a number of constants that can be used literally in scripts + +* `null`: nothing, zilch, not even false +* `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 + +Apart from that, there is a bunch of system variables, that start with `_` that are set by `scarpet` built-ins, +like `_`, typically each consecutive value in loops, `_i` indicating iteration, or `_a` like an accumulator +for `reduce` function. Certain calls to Minecraft specific calls would also set `_x`, `_y`, `_z`, indicating +block positions. All variables starting with `_` are read-only, and cannot be declared and modified in client code. + +## Literals + +`scarpet` accepts numeric and string liters constants. Numbers look like `1, 2.5, -3e-7, 0xff,` and are internally +represented primarily as Java's `double` but `scarpet` will try to trim trailing zeros as much as possible so if you +need to use them as integers or even longs - you can. Long values will also not loose their long precision in addition, +subtraction, negation and multiplication, however any other operation that is not guaranteed to return a long value +(like division) on a number even if it can be properly +represented as long, will make them convert to doubles. + +Strings use single quoting, for multiple reasons, but primarily to allow for +easier use of strings inside doubly quoted command arguments (when passing a script as a parameter of `/script fill` +for example), or when typing in jsons inside scarpet (to feed back into a `/data merge` command for example). +Strings also use backslashes `\` for quoting special characters, in both plain strings and regular expressions + +
+'foo'
+print('This doesn\'t work')
+nbt ~ '\\.foo'   // matching '.' as a '.', not 'any character match'
+
diff --git a/docs/scarpet/resources/editors/idea/1.txt b/docs/scarpet/resources/editors/idea/1.txt new file mode 100644 index 0000000..5f9a7dc --- /dev/null +++ b/docs/scarpet/resources/editors/idea/1.txt @@ -0,0 +1,127 @@ +abs +acos +acosh +acot +all +and +asin +asinh +atan +atan2 +atanh +bitwise_and +bitwise_or +bitwise_xor +bitwise_shift_left +bitwise_shift_right +bitwise_roll_left +bitwise_roll_right +bitwise_not +bitwise_popcount +bool +break +call +ceil +c_for +continue +copy +cos +cosh +cot +coth +csc +csch +decreasing +deg +delete +difference +double_to_long_bits +equal +exit +fact +filter +first +floor +for +get +has +hash_code +if +import +increasing +join +keys +l +length +log +log10 +log1p +long_bits_to_double +loop +lower +m +mandelbrot +map +max +min +nondecreasing +nonincreasing +not +number +or +outer +pairs +perlin +print +product +profile_expr +put +quotient +rad +rand +range +reduce +relu +replace +replace_first +reset_seed +return +round +sec +sech +simplex +sin +sinh +sleep +slice +sort +sort_key +sum +synchronize +system_variable_get +system_variable_set +split +sqrt +str +tan +tanh +task +task_completed +task_count +task_dock +task_join +task_thread +task_value +then +throw +time +title +try +type +undef +unique +upper +values +var +vars +while diff --git a/docs/scarpet/resources/editors/idea/2.txt b/docs/scarpet/resources/editors/idea/2.txt new file mode 100644 index 0000000..d90729f --- /dev/null +++ b/docs/scarpet/resources/editors/idea/2.txt @@ -0,0 +1,142 @@ +air +add_chunk_ticket +biome +blast_resistance +block +block_data +block_light +block_list +block_sound +block_state +block_tick +block_tags +blocks_daylight +blocks_movement +bossbar +brightness +chunk_tickets +close_screen +convert_date +crafting_remaining_item +create_datapack +create_explosion +create_marker +create_screen +current_dimension +day_time +delete_file +destroy +diamond +display_title +draw_shape +drop_item +effective_light +emitted_light +encode_nbt +entity_area +entity_event +entity_id +entity_list +entity_load_handler +entity_selector +entity_types +escape_nbt +flammable +format +game_tick +generation_status +handle_event +hardness +harvest +in_dimension +in_slime_chunk +inhabited_time +inventory_find +inventory_get +inventory_remove +inventory_set +inventory_size +item_display_name +item_list +item_tags +last_tick_times +light +liquid +list_files +load_app_data +loaded +loaded_status +logger +map_colour +material +modify +nbt +nbt_storage +neighbours +opacity +parse_nbt +particle +particle_line +particle_rect +place_item +player +plop +poi +pos +pos_offset +power +query +random_tick +recipe_data +rect +reload_chunk +remove_all_markers +reset_chunk +run +sample_noise +save +scan +schedule +scoreboard +scoreboard_add +scoreboard_remove +scoreboard_display +scoreboard_property +screen_property +tag_matches +team_add +team_leave +team_list +team_property +team_remove +read_file +see_sky +set +set_biome +set_poi +set_structure +signal_event +sky_light +solid +sound +spawn +spawn_potential +stack_limit +statistic +store_app_data +structures +structure_eligibility +structure_references +suffocates +system_info +tick_time +ticks_randomly +top +transparent +update +unix_time +view_distance +volume +without_updates +world_time +write_file diff --git a/docs/scarpet/resources/editors/idea/3.txt b/docs/scarpet/resources/editors/idea/3.txt new file mode 100644 index 0000000..fc9e16f --- /dev/null +++ b/docs/scarpet/resources/editors/idea/3.txt @@ -0,0 +1,12 @@ +_ +_a +_i +_x +_y +_z +_trace +euler +false +null +pi +true \ No newline at end of file diff --git a/docs/scarpet/resources/editors/idea/4.txt b/docs/scarpet/resources/editors/idea/4.txt new file mode 100644 index 0000000..29bcdb3 --- /dev/null +++ b/docs/scarpet/resources/editors/idea/4.txt @@ -0,0 +1,51 @@ +__command() -> +__config() -> +__on_chunk_generated(x, z) -> +__on_chunk_loaded(x, z) -> +__on_close() -> +__on_player_attacks_entity(player, entity) -> +__on_player_breaks_block(player, block) -> +__on_player_clicks_block(player, block, face) -> +__on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension) -> +__on_player_chooses_recipe(player, recipe, full_stack) -> +__on_player_collides_with_entity(player, entity) -> +__on_player_deploys_elytra(player) -> +__on_player_drops_item(player) -> +__on_player_drops_stack(player) -> +__on_player_finishes_using_item(player, item_tuple, hand) -> +__on_player_interacts_with_block(player, hand, block, face, hitvec) -> +__on_player_interacts_with_entity(player, entity, hand) -> +__on_player_jumps(player) -> +__on_player_picks_up_item(player, item) -> +__on_player_placing_block(player, item_tuple, hand, block) -> +__on_player_places_block(player, item_tuple, hand, block) -> +__on_player_releases_item(player, item_tuple, hand) -> +__on_player_rides(player, forward, strafe, jumping, sneaking) -> +__on_player_right_clicks_block(player, item_tuple, hand, block, face, hitvec) -> +__on_player_starts_sneaking(player) -> +__on_player_starts_sprinting(player) -> +__on_player_stops_sneaking(player) -> +__on_player_stops_sprinting(player) -> +__on_player_swings_hand(player, hand) -> +__on_player_switches_slot(player, from, to) -> +__on_player_swaps_hands(player) -> +__on_player_takes_damage(player, amount, source, entity) -> +__on_player_trades(player, entity, buy_left, buy_right, sell) -> +__on_player_uses_item(player, item_tuple, hand) -> +__on_player_wakes_up(player) -> +__on_player_escapes_sleep(player) -> +__on_statistic(player, category, event, value) -> +__on_start() -> +__on_tick() -> +__on_tick_ender() -> +__on_tick_nether() -> +__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_disconnects(player, reason) -> +__on_player_connects(player) -> +__on_player_message(player, message) -> +__on_player_command(player, command) -> +__on_explosion(pos, power, source, causer, mode, fire) -> +__on_explosion_outcome(pos, power, source, causer, mode, fire, blocks, entities) -> diff --git a/docs/scarpet/resources/editors/idea/Idea.md b/docs/scarpet/resources/editors/idea/Idea.md new file mode 100644 index 0000000..36218ca --- /dev/null +++ b/docs/scarpet/resources/editors/idea/Idea.md @@ -0,0 +1,16 @@ +# Custom Language Support for Intellij + +### Add scarpet to registered file types (`.sc` and `.scl`) + +![Settings -> File Types](/docs/scarpet/resources/media/idea_settings.png) + +### When adding use the following settings + +![Customize your settings](/docs/scarpet/resources/media/idea_customize.png) + +### Grab lists of keywords (1,2,3 and 4) from the following files: + +[1](/docs/scarpet/resources/editors/idea/1.txt) +[2](/docs/scarpet/resources/editors/idea/2.txt) +[3](/docs/scarpet/resources/editors/idea/3.txt) +[4](/docs/scarpet/resources/editors/idea/4.txt) diff --git a/docs/scarpet/resources/editors/npp/scarpet.xml b/docs/scarpet/resources/editors/npp/scarpet.xml new file mode 100644 index 0000000..8d84d45 --- /dev/null +++ b/docs/scarpet/resources/editors/npp/scarpet.xml @@ -0,0 +1,186 @@ + + + + + + + + 00// 01 02 03 04 + 0x + + + + + + + ; + - * / % ^ && || > >= < <= == != = += -> <> ! ~ , : ... + + ( + + ) + + + + + + + + not fact rand reset_seed + sin cos tan asin acos atan atan2 sinh cosh tanh sec csc sech csch cot acot coth asinh acosh atanh + rad deg + log log10 log1p + sqrt + max min abs round floor ceil + then sum difference product quotient equal unique increasing decreasing nonincreasing nondecreasing or and + bitwise_or bitwise_and bitwise_xor bitwise_shift_left bitwise_shift_right bitwise_roll_left bitwise_roll_right bitwise_not bitwise_popcount + double_to_long_bits long_bits_to_double + mandelbrot relu + perlin simplex + + print return exit throw try + + task task_thread task_count task_value task_join task_completed synchronize task_dock + + system_variable_get system_variable_set + + l join split slice sort sort_key range m keys values pairs copy + + get put has delete + + var undef vars if loop map filter first all c_for for while reduce continue break + + import call outer type hash_code bool number str length sleep time profile_expr lower upper title replace replace_first + + + block pos pos_offset solid air liquid flammable + transparent opacity blocks_daylight emitted_light light block_light sky_light effective_light see_sky brightness + 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 block_state block_list block_tags block_data poi set_poi + place_item set_biome biome loaded_status generation_status sample_noise inhabited_time spawn_potential chunk_tickets + plop harvest destroy create_explosion + structure_eligibility structures structure_references set_structure + reset_chunk reload_chunk create_datapack + add_chunk_ticket + + convert_date unix_time + + display_title + + scoreboard scoreboard_add scoreboard_remove scoreboard_display scoreboard_property + + team_add team_list team_remove team_leave team_property + + create_screen close_screen screen_property + + bossbar + + player spawn entity_id entity_list entity_area entity_selector query modify entity_event entity_load_handler entity_types + + item_list item_tags stack_limit recipe_data crafting_remaining_item item_display_name + + inventory_size inventory_has_items inventory_get inventory_set inventory_find inventory_remove drop_item + + scan volume neighbours rect diamond + + format logger sound particle particle_line particle_rect system_info + + handle_event signal_event + + run save tick_time world_time day_time last_tick_times game_tick current_dimension in_dimension view_distance + + schedule nbt draw_shape create_marker remove_all_markers + + load_app_data store_app_data read_file write_file delete_file list_files + escape_nbt parse_nbt encode_nbt tag_matches statistic + + nbt_storage + + euler pi null true false + x y z p + _ _i _a _x _y _z _trace + global_ + + __command() -> + __config() -> + __on_chunk_generated(x, z) -> + __on_chunk_loaded(x, z) -> + __on_close() -> + __on_player_attacks_entity(player, entity) -> + __on_player_breaks_block(player, block) -> + __on_player_clicks_block(player, block, face) -> + __on_player_changes_dimension(player, from_pos, from_dimension, to_pos, to_dimension) -> + __on_player_chooses_recipe(player, recipe, full_stack) -> + __on_player_collides_with_entity(player, entity) -> + __on_player_deploys_elytra(player) -> + __on_player_drops_item(player) -> + __on_player_drops_stack(player) -> + __on_player_finishes_using_item(player, item_tuple, hand) -> + __on_player_interacts_with_block(player, hand, block, face, hitvec) -> + __on_player_interacts_with_entity(player, entity, hand) -> + __on_player_jumps(player) -> + __on_player_picks_up_item(player, item) -> + __on_player_placing_block(player, item_tuple, hand, block) -> + __on_player_places_block(player, item_tuple, hand, block) -> + __on_player_releases_item(player, item_tuple, hand) -> + __on_player_rides(player, forward, strafe, jumping, sneaking) -> + __on_player_right_clicks_block(player, item_tuple, hand, block, face, hitvec) -> + __on_player_starts_sneaking(player) -> + __on_player_starts_sprinting(player) -> + __on_player_stops_sneaking(player) -> + __on_player_stops_sprinting(player) -> + __on_player_swings_hand(player, hand) -> + __on_player_switches_slot(player, from, to) -> + __on_player_swaps_hands(player) -> + __on_player_takes_damage(player, amount, source, entity) -> + __on_player_trades(player, entity, buy_left, buy_right, sell) -> + __on_player_uses_item(player, item_tuple, hand) -> + __on_player_wakes_up(player) -> + __on_player_escapes_sleep(player) -> + __on_statistic(player, category, event, value) -> + __on_start() -> + __on_tick() -> + __on_tick_ender() -> + __on_tick_nether() -> + __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_disconnects(player, reason) -> + __on_player_connects(player) -> + __on_player_message(player, message) -> + __on_player_command(player, command) -> + __on_explosion(pos, power, source, causer, mode, fire) -> + __on_explosion_outcome(pos, power, source, causer, mode, fire, blocks, entities) -> + + $ + 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/docs/scarpet/resources/editors/npp/scarpet_v1.0_notepad++.xml b/docs/scarpet/resources/editors/npp/scarpet_v1.0_notepad++.xml new file mode 100644 index 0000000..6945ced --- /dev/null +++ b/docs/scarpet/resources/editors/npp/scarpet_v1.0_notepad++.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + ; + - * / % ^ && || > >= < <= == != = += -> <> ! , ~ + + ( + + ) + + + + + + + not fact rand sin cos tan asin acos atan atan2 sinh cosh tanh sec csc sech csch cot acot coth asinh acosh atanh rad deg log log10 log1p sqrt max min abs round floor ceil mandelbrot relu print return exit throw try l join split slice sort sort_key range element put var undef vars if loop map filter first all for while reduce outer bool number str length rand sleep time + block pos solid air liquid flammable transparent opacity blocks_daylight emitted_light light block_light sky_light see_sky hardness blast_resistance top loaded loaded_ep suffocates power ticks_randomly update block_tick random_tick set blocks_movement block_sound material map_colour property player entity_id entity_list entity_area entity_selector query modify scan volume neighbours rect diamond sound particle particle_line particle_rect run save tick_time game_tick plop harvest destroy schedule + euler pi null true false + x y z p + _ _i _a _x _y _z + global_ + __command __on_player_jumps __on_player_deploys_elytra __on_player_wakes_up __on_player_rides __on_player_uses_item __on_player_clicks_block __on_player_right_clicks_block __on_player_breaks_block __on_player_interacts_with_entity __on_player_attacks_entity __on_player_starts_sneaking __on_player_stops_sneaking __on_player_starts_sprinting __on_player_stops_sprinting + $ + 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/docs/scarpet/resources/editors/npp/scarpet_v1.4_notepad++.xml b/docs/scarpet/resources/editors/npp/scarpet_v1.4_notepad++.xml new file mode 100644 index 0000000..e704c02 --- /dev/null +++ b/docs/scarpet/resources/editors/npp/scarpet_v1.4_notepad++.xml @@ -0,0 +1,64 @@ + + + + + + + + 00// 01 02 03 04 + + + + + + + + ; + - * / % ^ && || > >= < <= == != = += -> <> ! , ~ + + ( + + ) + + + + + + + not fact rand sin cos tan asin acos atan atan2 sinh cosh tanh sec csc sech csch cot acot coth asinh acosh atanh rad deg log log10 log1p sqrt max min abs round floor ceil mandelbrot relu print return exit throw try l join split slice sort sort_key range get put var undef vars if loop map filter first all for while reduce outer type bool number str length rand sleep time + block pos solid air liquid flammable transparent opacity blocks_daylight emitted_light light block_light sky_light see_sky hardness blast_resistance top loaded loaded_ep suffocates power ticks_randomly update block_tick random_tick set blocks_movement block_sound material map_colour property block_properties block_data place_item set_biome player spawn entity_id entity_list entity_area entity_selector query modify stack_limit inventory_size inventory_get inventory_set inventory_find inventory_remove drop_item scan volume neighbours rect diamond sound particle particle_line particle_rect run save tick_time game_tick plop harvest destroy schedule nbt create_marker remove_all_markers + euler pi null true false + x y z p + _ _i _a _x _y _z + global_ + __command __on_player_jumps __on_player_deploys_elytra __on_player_wakes_up __on_player_rides __on_player_uses_item __on_player_clicks_block __on_player_right_clicks_block __on_player_breaks_block __on_player_interacts_with_entity __on_player_attacks_entity __on_player_starts_sneaking __on_player_stops_sneaking __on_player_starts_sprinting __on_player_stops_sprinting __on_tick __on_tick_ender __on_tick_nether + $ + 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/docs/scarpet/resources/editors/npp/scarpet_v1.5_notepad++.xml b/docs/scarpet/resources/editors/npp/scarpet_v1.5_notepad++.xml new file mode 100644 index 0000000..b013b24 --- /dev/null +++ b/docs/scarpet/resources/editors/npp/scarpet_v1.5_notepad++.xml @@ -0,0 +1,64 @@ + + + + + + + + 00// 01 02 03 04 + 0x + + + + + + + ; + - * / % ^ && || > >= < <= == != = += -> <> ! ~ , : + + ( + + ) + + + + + + + not fact rand sin cos tan asin acos atan atan2 sinh cosh tanh sec csc sech csch cot acot coth asinh acosh atanh rad deg log log10 log1p sqrt max min abs round floor ceil mandelbrot relu print return exit throw try l join split slice sort sort_key range m keys values pairs get put has delete var undef vars if loop map filter first all for while reduce outer type bool number str length rand sleep time profile_expr lower upper title + block pos pos_offset solid air liquid flammable transparent opacity blocks_daylight emitted_light light block_light sky_light see_sky hardness blast_resistance top loaded loaded_ep suffocates power ticks_randomly update block_tick random_tick set blocks_movement block_sound material map_colour property block_properties block_data place_item set_biome biome player spawn entity_id entity_list entity_area entity_selector query modify entity_event stack_limit item_category inventory_size inventory_has_items inventory_get inventory_set inventory_find inventory_remove drop_item scan volume neighbours rect diamond logger sound particle particle_line particle_rect run save tick_time game_tick current_dimension in_dimension plop harvest destroy schedule nbt create_marker remove_all_markers load_app_data store_app_data + euler pi null true false + x y z p + _ _i _a _x _y _z + global_ + __command __config __on_player_jumps __on_player_deploys_elytra __on_player_wakes_up __on_player_rides __on_player_uses_item __on_player_clicks_block __on_player_right_clicks_block __on_player_breaks_block __on_player_interacts_with_entity __on_player_attacks_entity __on_player_starts_sneaking __on_player_stops_sneaking __on_player_starts_sprinting __on_player_stops_sprinting __on_player_releases_item __on_player_finishes_using_item __on_player_drops_item __on_player_drops_stack __on_tick __on_tick_ender __on_tick_nether + $ + 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/docs/scarpet/resources/editors/npp/scarpet_v1.6_notepad++.xml b/docs/scarpet/resources/editors/npp/scarpet_v1.6_notepad++.xml new file mode 100644 index 0000000..d8058d2 --- /dev/null +++ b/docs/scarpet/resources/editors/npp/scarpet_v1.6_notepad++.xml @@ -0,0 +1,64 @@ + + + + + + + + 00// 01 02 03 04 + 0x + + + + + + + ; + - * / % ^ && || > >= < <= == != = += -> <> ! ~ , : + + ( + + ) + + + + + + + not fact rand sin cos tan asin acos atan atan2 sinh cosh tanh sec csc sech csch cot acot coth asinh acosh atanh rad deg log log10 log1p sqrt max min abs round floor ceil mandelbrot relu print return exit throw try l join split slice sort sort_key range m keys values pairs copy get put has delete var undef vars if loop map filter first all for while reduce continue break call outer type bool number str length rand sleep time profile_expr lower upper title replace replace_first + block pos pos_offset solid air liquid flammable transparent opacity blocks_daylight emitted_light light block_light sky_light see_sky hardness blast_resistance top loaded suffocates power ticks_randomly update block_tick random_tick set blocks_movement block_sound material map_colour property block_properties block_data place_item set_biome biome loaded_status generation_status chunk_tickets player spawn entity_id entity_list entity_area entity_selector query modify entity_event stack_limit item_category inventory_size inventory_has_items inventory_get inventory_set inventory_find inventory_remove drop_item scan volume neighbours rect diamond logger sound particle particle_line particle_rect run save tick_time game_tick current_dimension in_dimension plop harvest destroy schedule nbt create_marker remove_all_markers load_app_data store_app_data escape_nbt + euler pi null true false + x y z p + _ _i _a _x _y _z + global_ + __command __config __on_player_jumps __on_player_deploys_elytra __on_player_wakes_up __on_player_rides __on_player_uses_item __on_player_clicks_block __on_player_right_clicks_block __on_player_breaks_block __on_player_interacts_with_entity __on_player_attacks_entity __on_player_starts_sneaking __on_player_stops_sneaking __on_player_starts_sprinting __on_player_stops_sprinting __on_player_releases_item __on_player_finishes_using_item __on_player_drops_item __on_player_drops_stack __on_tick __on_tick_ender __on_tick_nether + $ + 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/docs/scarpet/resources/editors/npp/scarpet_v1.7_notepad++.xml b/docs/scarpet/resources/editors/npp/scarpet_v1.7_notepad++.xml new file mode 100644 index 0000000..a24fe8f --- /dev/null +++ b/docs/scarpet/resources/editors/npp/scarpet_v1.7_notepad++.xml @@ -0,0 +1,154 @@ + + + + + + + + 00// 01 02 03 04 + 0x + + + + + + + ; + - * / % ^ && || > >= < <= == != = += -> <> ! ~ , : + + ( + + ) + + + + + + + + not fact rand + sin cos tan asin acos atan atan2 sinh cosh tanh sec csc sech csch cot acot coth asinh acosh atanh + rad deg + log log10 log1p + sqrt + max min abs round floor ceil + mandelbrot relu + perlin simplex + + print return exit throw try + + task task_count task_value task_join task_completed synchronize task_dock + + system_variable_get system_variable_set + + l join split slice sort sort_key range m keys values pairs copy + + get put has delete + + var undef vars if loop map filter first all c_for for while reduce continue break + + import call outer type hash_code bool number str length rand sleep time profile_expr lower upper title replace replace_first + + + block pos pos_offset solid air liquid flammable + transparent opacity blocks_daylight emitted_light light block_light sky_light see_sky brightness + 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 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 + + team_add team_list team_remove team_leave team_property + + player spawn entity_id entity_list entity_area entity_selector query modify entity_event + + stack_limit item_category recipe_data crafting_remaining_item + + inventory_size inventory_has_items inventory_get inventory_set inventory_find inventory_remove drop_item + + scan volume neighbours rect diamond + + format logger sound particle particle_line particle_rect + + 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 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 + _ _i _a _x _y _z + global_ + + __command + __config + __on_chunk_generated + __on_close + __on_player_attacks_entity + __on_player_breaks_block + __on_player_clicks_block + __on_player_changes_dimension + __on_player_chooses_recipe + __on_player_deploys_elytra + __on_player_drops_item + __on_player_drops_stack + __on_player_finishes_using_item + __on_player_interacts_with_block + __on_player_interacts_with_entity + __on_player_jumps + __on_player_places_block + __on_player_releases_item + __on_player_rides + __on_player_right_clicks_block + __on_player_starts_sneaking + __on_player_starts_sprinting + __on_player_stops_sneaking + __on_player_stops_sprinting + __on_player_switches_slot + __on_player_uses_item + __on_player_wakes_up + __on_statistic + __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/docs/scarpet/resources/media/idea_customize.png b/docs/scarpet/resources/media/idea_customize.png new file mode 100644 index 0000000..26cbc2c Binary files /dev/null and b/docs/scarpet/resources/media/idea_customize.png differ diff --git a/docs/scarpet/resources/media/idea_settings.png b/docs/scarpet/resources/media/idea_settings.png new file mode 100644 index 0000000..255f8f4 Binary files /dev/null and b/docs/scarpet/resources/media/idea_settings.png differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..fcf94f0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,28 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G + +# Fabric Properties + # check https://fabricmc.net/develop/ + minecraft_version=1.21 + loader_version=0.15.11 + jsr305_version=3.0.2 + fabric_version=0.99.2+1.21 + +# Mod Properties + mod_version = 1.4.147 + maven_group = carpet + archives_base_name = fabric-carpet + +# Release Action properties for Curseforge and Snapshots + # The Curseforge versions "names" or ids for the main branch (comma separated: 1.16.4,1.16.5) + # This is needed because CF uses too vague names for prereleases and release candidates + # Can also be the version ID directly coming from https://minecraft.curseforge.com/api/game/versions?token=[API_TOKEN] + release-curse-versions = Minecraft 1.21:1.21 + # Whether or not to build another branch on release + release-extra-branch = false + # The name of the second branch to release + release-extra-branch-name = 1.20.4 + # The "name" or id of the Curseforge version for the secondary branch + # This is needed because CF uses too vague names for snapshots + # Can also be the version ID directly coming from https://minecraft.curseforge.com/api/game/versions?token=[API_TOKEN] + release-extra-curse-version = Minecraft 1.20:1.20.4-Snapshot \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..943f0cb Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..509c4a2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..65dcd68 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..93274d1 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,3 @@ +before_install: +- sdk install java 21.0.2-open +- sdk use java 21.0.2-open diff --git a/mergedoc.bat b/mergedoc.bat new file mode 100644 index 0000000..1655943 --- /dev/null +++ b/mergedoc.bat @@ -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/mergedoc.sh b/mergedoc.sh new file mode 100644 index 0000000..1655943 --- /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/settings.gradle b/settings.gradle new file mode 100644 index 0000000..ee536dc --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven { // for all the fabric stuffs + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/carpet/CarpetExtension.java b/src/main/java/carpet/CarpetExtension.java new file mode 100644 index 0000000..e7a742c --- /dev/null +++ b/src/main/java/carpet/CarpetExtension.java @@ -0,0 +1,170 @@ +package carpet; + +import carpet.script.CarpetExpression; +import carpet.api.settings.SettingsManager; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collections; +import java.util.Map; + +public interface CarpetExtension +{ + /** + * When game started before world is loaded + */ + default void onGameStarted() {} + + /** + * Runs once per loaded world once the server / gamerules etc are fully loaded + * but before worlds are loaded + * Can be loaded multiple times in SinglePlayer + * + * @param server The {@link MinecraftServer} instance that loaded + */ + default void onServerLoaded(MinecraftServer server) {} + + /** + * Runs once per loaded world once the World files are fully loaded + * Can be loaded multiple times in SinglePlayer + * + * @param server The current {@link MinecraftServer} instance + * + */ + default void onServerLoadedWorlds(MinecraftServer server) {} + + /** + * Runs once per game tick, as a first thing in the tick + * + * @param server The current {@link MinecraftServer} instance + * + */ + default void onTick(MinecraftServer server) {} + + /** + * Register your own commands right after vanilla commands are added + * If that matters for you + * + * Deprecated, Implement {@link CarpetExtension#registerCommands(CommandDispatcher, CommandBuildContext)} + * + * @param dispatcher The current {@link CommandSource} dispatcher + * where you should register your commands + * + */ + @Deprecated(forRemoval = true) + default void registerCommands(CommandDispatcher dispatcher) {} + + /** + * Register your own commands right after vanilla commands are added + * If that matters for you + * + * @param dispatcher The current {@link CommandDispatcher} dispatcher + * where you should register your commands + * @param commandBuildContext The current {@link CommandBuildContext} context + * * which you can use for registries lookup + */ + default void registerCommands(CommandDispatcher dispatcher, final CommandBuildContext commandBuildContext) { + registerCommands(dispatcher); + } + + /** + * @deprecated Implement {@link #extensionSettingsManager()} instead + */ + @Deprecated(forRemoval = true) + default carpet.settings.SettingsManager customSettingsManager() {return null;} + + /** + * Provide your own custom settings manager managed in the same way as base /carpet + * command, but separated to its own command as defined in SettingsManager. + * + * @return Your custom {@link SettingsManager} instance to be managed by Carpet + * + */ + default SettingsManager extensionSettingsManager() { + // Warn extensions overriding the other (deprecated) method, go ahead and override this if you want to provide a custom SettingsManager + SettingsManager deprecatedManager = customSettingsManager(); + if (deprecatedManager != null) { + // Extension is providing a manager via the old method (and also hasn't overriden this) + CarpetServer.warnOutdatedManager(this); + } + return customSettingsManager(); + } + + /** + * Event that gets called when a player logs in + * + * @param player The {@link ServerPlayer} that logged in + * + */ + default void onPlayerLoggedIn(ServerPlayer player) {} + + /** + * Event that gets called when a player logs out + * + * @param player The {@link ServerPlayer} that logged out + * + */ + default void onPlayerLoggedOut(ServerPlayer player) {} + + /** + * Event that gets called when the server closes. + * Can be called multiple times in singleplayer + * + * @param server The {@link MinecraftServer} that is closing + * + */ + default void onServerClosed(MinecraftServer server) {} + + /** + * Event that gets called when the server reloads (usually + * when running the /reload command) + * + * @param server The {@link MinecraftServer} that is reloading + * + */ + default void onReload(MinecraftServer server) {} + + /** + * @return A {@link String} usually being the extension's id + * + */ + default String version() {return null;} + + /** + * Called when registering custom loggers + * + */ + default void registerLoggers() {} + + /** + * Method for Carpet to get the translations for extension's + * rules. + * + * @param lang A {@link String} being the language id selected by the user + * @return A {@link Map} containing the string key with its + * respective translation {@link String} or an empty map if not available + * + */ + default Map canHasTranslations(String lang) { return Collections.emptyMap();} + + /** + * Handles each call that creates / parses the scarpet expression. + * Extensions can add their own built-in functions here. + * + * Events such as generic events or entity events, can be added statically + * by creating new events as + * + * CarpetEventServer.Event class: to handle `__on_foo()` type of call definitions + * + * or + * + * EntityEventsGroup.Event class: to handle `entity_event('foo', ...)` type of events + * + * @param expression Passed {@link CarpetExpression} to add built-in functions to + */ + default void scarpetApi(CarpetExpression expression) {} + +} diff --git a/src/main/java/carpet/CarpetServer.java b/src/main/java/carpet/CarpetServer.java new file mode 100644 index 0000000..a6f50a6 --- /dev/null +++ b/src/main/java/carpet/CarpetServer.java @@ -0,0 +1,238 @@ +package carpet; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import carpet.commands.CounterCommand; +import carpet.commands.DistanceCommand; +import carpet.commands.DrawCommand; +import carpet.commands.InfoCommand; +import carpet.commands.LogCommand; +import carpet.commands.MobAICommand; +import carpet.commands.PerimeterInfoCommand; +import carpet.commands.PlayerCommand; +import carpet.commands.ProfileCommand; +import carpet.script.ScriptCommand; +import carpet.commands.SpawnCommand; +import carpet.commands.TestCommand; +import carpet.network.ServerNetworkHandler; +import carpet.helpers.HopperCounter; +import carpet.logging.LoggerRegistry; +import carpet.script.CarpetScriptServer; +import carpet.api.settings.SettingsManager; +import carpet.logging.HUDController; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.utils.ParticleParser; +import carpet.utils.MobAI; +import carpet.utils.SpawnReporter; +import com.mojang.brigadier.CommandDispatcher; + +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.commands.PerfCommand; +import net.minecraft.server.level.ServerPlayer; + +public class CarpetServer // static for now - easier to handle all around the code, its one anyways +{ + public static MinecraftServer minecraft_server; + public static CarpetScriptServer scriptServer; + public static carpet.settings.SettingsManager settingsManager; // to change type to api type, can't change right now because of binary and source compat + public static final List extensions = new ArrayList<>(); + + /** + * Registers a {@link CarpetExtension} to be managed by Carpet.
+ * Should be called before Carpet's startup, like in Fabric Loader's + * {@link net.fabricmc.api.ModInitializer} entrypoint + * @param extension The instance of a {@link CarpetExtension} to be registered + */ + public static void manageExtension(CarpetExtension extension) + { + extensions.add(extension); + // Stop the stupid practice of extensions mixing into Carpet just to register themselves + if (StackWalker.getInstance().walk(stream -> stream.skip(1) + .anyMatch(el -> el.getClassName() == CarpetServer.class.getName()))) + { + CarpetSettings.LOG.warn(""" + Extension '%s' is registering itself using a mixin into Carpet instead of a regular ModInitializer! + This is stupid and will crash the game in future versions!""".formatted(extension.getClass().getSimpleName())); + } + } + + // Separate from onServerLoaded, because a server can be loaded multiple times in singleplayer + // Gets called by Fabric Loader from a ServerModInitializer and a ClientModInitializer, in both to allow extensions + // to register before this call in a ModInitializer (declared in fabric.mod.json) + public static void onGameStarted() + { + settingsManager = new carpet.settings.SettingsManager(CarpetSettings.carpetVersion, "carpet", "Carpet Mod"); + settingsManager.parseSettingsClass(CarpetSettings.class); + extensions.forEach(CarpetExtension::onGameStarted); + //FabricAPIHooks.initialize(); + CarpetScriptServer.parseFunctionClasses(); + CarpetSettings.LOG.info("CARPET PVP LOADED"); + } + + public static void onServerLoaded(MinecraftServer server) + { + CarpetServer.minecraft_server = server; + // shoudl not be needed - that bit needs refactoring, but not now. + SpawnReporter.resetSpawnStats(server, true); + + forEachManager(sm -> sm.attachServer(server)); + extensions.forEach(e -> e.onServerLoaded(server)); + scriptServer = new CarpetScriptServer(server); + Carpet.MinecraftServer_addScriptServer(server, scriptServer); + MobAI.resetTrackers(); + LoggerRegistry.initLoggers(); + //TickSpeed.reset(); + } + + public static void onServerLoadedWorlds(MinecraftServer minecraftServer) + { + HopperCounter.resetAll(minecraftServer, true); + extensions.forEach(e -> e.onServerLoadedWorlds(minecraftServer)); + // initialize scarpet rules after all extensions are loaded + forEachManager(SettingsManager::initializeScarpetRules); + scriptServer.initializeForWorld(); + } + + public static void tick(MinecraftServer server) + { + HUDController.update_hud(server, null); + if (scriptServer != null) scriptServer.tick(); + + //in case something happens + CarpetSettings.impendingFillSkipUpdates.set(false); + + extensions.forEach(e -> e.onTick(server)); + } + + public static void registerCarpetCommands(CommandDispatcher dispatcher, Commands.CommandSelection environment, CommandBuildContext commandBuildContext) + { + if (settingsManager == null) // bootstrap dev initialization check + { + return; + } + forEachManager(sm -> sm.registerCommand(dispatcher, commandBuildContext)); + + ProfileCommand.register(dispatcher, commandBuildContext); + CounterCommand.register(dispatcher, commandBuildContext); + LogCommand.register(dispatcher, commandBuildContext); + SpawnCommand.register(dispatcher, commandBuildContext); + PlayerCommand.register(dispatcher, commandBuildContext); + InfoCommand.register(dispatcher, commandBuildContext); + DistanceCommand.register(dispatcher, commandBuildContext); + PerimeterInfoCommand.register(dispatcher, commandBuildContext); + DrawCommand.register(dispatcher, commandBuildContext); + ScriptCommand.register(dispatcher, commandBuildContext); + MobAICommand.register(dispatcher, commandBuildContext); + // registering command of extensions that has registered before either server is created + // for all other, they will have them registered when they add themselves + extensions.forEach(e -> { + e.registerCommands(dispatcher, commandBuildContext); + }); + + if (environment != Commands.CommandSelection.DEDICATED) + PerfCommand.register(dispatcher); + + if (FabricLoader.getInstance().isDevelopmentEnvironment()) + TestCommand.register(dispatcher); + // todo 1.16 - re-registerer apps if that's a reload operation. + } + + public static void onPlayerLoggedIn(ServerPlayer player) + { + ServerNetworkHandler.onPlayerJoin(player); + LoggerRegistry.playerConnected(player); + extensions.forEach(e -> e.onPlayerLoggedIn(player)); + scriptServer.onPlayerJoin(player); + } + + public static void onPlayerLoggedOut(ServerPlayer player, Component reason) + { + ServerNetworkHandler.onPlayerLoggedOut(player); + LoggerRegistry.playerDisconnected(player); + extensions.forEach(e -> e.onPlayerLoggedOut(player)); + // first case client, second case server + CarpetScriptServer runningScriptServer = (player.getServer() == null) ? scriptServer : Vanilla.MinecraftServer_getScriptServer(player.getServer()); + if (runningScriptServer != null && !runningScriptServer.stopAll) { + runningScriptServer.onPlayerLoggedOut(player, reason); + } + } + + public static void clientPreClosing() + { + if (scriptServer != null) scriptServer.onClose(); + scriptServer = null; + } + + public static void onServerClosed(MinecraftServer server) + { + // this for whatever reason gets called multiple times even when joining on SP + // so we allow to pass multiple times gating it only on existing server ref + if (minecraft_server != null) + { + if (scriptServer != null) scriptServer.onClose(); + // this is a mess, will cleanip onlly when global reference is gone + if (!Vanilla.MinecraftServer_getScriptServer(server).stopAll) { + Vanilla.MinecraftServer_getScriptServer(server).onClose(); + } + + scriptServer = null; + ServerNetworkHandler.close(); + + LoggerRegistry.stopLoggers(); + HUDController.resetScarpetHUDs(); + ParticleParser.resetCache(); + extensions.forEach(e -> e.onServerClosed(server)); + minecraft_server = null; + } + } + public static void onServerDoneClosing(MinecraftServer server) + { + forEachManager(SettingsManager::detachServer); + } + + // not API + // carpet's included + public static void forEachManager(Consumer consumer) + { + consumer.accept(settingsManager); + for (CarpetExtension e : extensions) + { + SettingsManager manager = e.extensionSettingsManager(); + if (manager != null) + { + consumer.accept(manager); + } + } + } + + public static void registerExtensionLoggers() + { + extensions.forEach(CarpetExtension::registerLoggers); + } + + public static void onReload(MinecraftServer server) + { + scriptServer.reload(server); + extensions.forEach(e -> e.onReload(server)); + } + + private static final Set warnedOutdatedManagerProviders = new HashSet<>(); + static void warnOutdatedManager(CarpetExtension ext) + { + if (warnedOutdatedManagerProviders.add(ext)) + CarpetSettings.LOG.warn(""" + %s is providing a SettingsManager from an outdated method in CarpetExtension! + This behaviour will not work in later Carpet versions and the manager won't be registered!""".formatted(ext.getClass().getName())); + } +} + diff --git a/src/main/java/carpet/CarpetSettings.java b/src/main/java/carpet/CarpetSettings.java new file mode 100644 index 0000000..d469d6d --- /dev/null +++ b/src/main/java/carpet/CarpetSettings.java @@ -0,0 +1,1037 @@ +package carpet; + +import carpet.api.settings.CarpetRule; +import carpet.api.settings.RuleCategory; +import carpet.api.settings.Validators; +import carpet.api.settings.Validator; +import carpet.script.utils.AppStoreManager; +import carpet.settings.Rule; +import carpet.utils.Translations; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.SemanticVersion; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerInterface; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.StructureBlockEntity; +import net.minecraft.world.level.block.piston.PistonStructureResolver; +import net.minecraft.world.level.border.BorderStatus; +import net.minecraft.world.level.border.WorldBorder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +import static carpet.api.settings.RuleCategory.BUGFIX; +import static carpet.api.settings.RuleCategory.COMMAND; +import static carpet.api.settings.RuleCategory.CREATIVE; +import static carpet.api.settings.RuleCategory.EXPERIMENTAL; +import static carpet.api.settings.RuleCategory.FEATURE; +import static carpet.api.settings.RuleCategory.OPTIMIZATION; +import static carpet.api.settings.RuleCategory.SURVIVAL; +import static carpet.api.settings.RuleCategory.TNT; +import static carpet.api.settings.RuleCategory.DISPENSER; +import static carpet.api.settings.RuleCategory.SCARPET; +import static carpet.api.settings.RuleCategory.CLIENT; + +@SuppressWarnings({"CanBeFinal", "removal"}) // removal should be removed after migrating rules to the new system +public class CarpetSettings +{ + public static final String carpetVersion = FabricLoader.getInstance().getModContainer("carpet").orElseThrow().getMetadata().getVersion().toString(); + public static final int [] releaseTarget = { + ((SemanticVersion)FabricLoader.getInstance().getModContainer("minecraft").orElseThrow().getMetadata().getVersion()).getVersionComponent(1), + ((SemanticVersion)FabricLoader.getInstance().getModContainer("minecraft").orElseThrow().getMetadata().getVersion()).getVersionComponent(2) + }; + public static final Logger LOG = LoggerFactory.getLogger("carpet pvp"); + public static final ThreadLocal skipGenerationChecks = ThreadLocal.withInitial(() -> false); + public static final ThreadLocal impendingFillSkipUpdates = ThreadLocal.withInitial(() -> false); + public static int runPermissionLevel = 2; + public static Block structureBlockIgnoredBlock = Blocks.STRUCTURE_VOID; + private static class LanguageValidator extends Validator { + @Override public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String string) { + if (!Translations.isValidLanguage(newValue)) + { + Messenger.m(source, "r "+newValue+" is not a valid language"); + return null; + } + CarpetSettings.language = newValue; + Translations.updateLanguage(); + return newValue; + } + } + @Rule( + desc = "Sets the language for Carpet", + category = FEATURE, + options = {"en_us", "fr_fr", "pt_br", "zh_cn", "zh_tw"}, + strict = true, // the current system doesn't handle fallbacks and other, not defined languages would make unreadable mess. Change later + validate = LanguageValidator.class + ) + public static String language = "en_us"; + + /* + These will be turned when events can be added / removed in code + Then also gotta remember to remove relevant rules + + @Rule( + desc = "Turns on internal camera path tracing app", + extra = "Controlled via 'camera' command", + category = {COMMAND, SCARPET}, + appSource = "camera" + ) + public static boolean commandCamera = true; + + @Rule( + desc = "Allows to add extra graphical debug information", + extra = "Controlled via 'overlay' command", + category = {COMMAND, SCARPET}, + appSource = "overlay" + ) + public static boolean commandOverlay = true; + + @Rule( + desc = "Turns on extra information about mobs above and around them", + extra = "Controlled via 'ai_tracker' command", + category = {COMMAND, SCARPET}, + appSource = "ai_tracker" + ) + public static boolean commandAITracker = true; + + @Rule( + desc = "Enables /draw commands", + extra = { + "... allows for drawing simple shapes or", + "other shapes which are sorta difficult to do normally" + }, + appSource = "draw", + category = {FEATURE, SCARPET, COMMAND} + ) + public static String commandDraw = "true"; + + @Rule( + desc = "Enables /distance command to measure in game distance between points", + extra = "Also enables brown carpet placement action if 'carpets' rule is turned on as well", + appSource = "distance", + category = {FEATURE, SCARPET, COMMAND} + ) + public static String commandDistance = "true"; + */ + + private static class CarpetPermissionLevel extends Validator { + @Override public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String string) { + if (source == null || source.hasPermission(4)) + return newValue; + return null; + } + + @Override + public String description() + { + return "This setting can only be set by admins with op level 4"; + } + } + @Rule( + desc = "Carpet command permission level. Can only be set via .conf file", + category = CREATIVE, + validate = CarpetPermissionLevel.class, + options = {"ops", "2", "4"} + ) + public static String carpetCommandPermissionLevel = "ops"; + + + @Rule(desc = "Gbhs sgnf sadsgras fhskdpri!!!", category = EXPERIMENTAL) + public static boolean superSecretSetting = false; + + @Rule(desc = "Dropping entire stacks works also from on the crafting UI result slot", category = {RuleCategory.BUGFIX, SURVIVAL}) + public static boolean ctrlQCraftingFix = false; + + @Rule(desc = "Parrots don't get of your shoulders until you receive proper damage", category = {SURVIVAL, FEATURE}) + public static boolean persistentParrots = false; + + /*@Rule( + desc = "Mobs growing up won't glitch into walls or go through fences", + category = BUGFIX, + validate = Validator.WIP.class + ) + public static boolean growingUpWallJump = false; + + @Rule( + desc = "Won't let mobs glitch into blocks when reloaded.", + extra = "Can cause slight differences in mobs behaviour", + category = {BUGFIX, EXPERIMENTAL}, + validate = Validator.WIP.class + ) + public static boolean reloadSuffocationFix = false; + */ + + @Rule( desc = "Players absorb XP instantly, without delay", category = CREATIVE ) + public static boolean xpNoCooldown = false; + + public static class StackableShulkerBoxValidator extends Validator + { + @Override + public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String string) + { + if (newValue.matches("^[0-9]+$")) { + int value = Integer.parseInt(newValue); + if (value <= 64 && value >= 2) { + shulkerBoxStackSize = value; + return newValue; + } + } + if (newValue.equalsIgnoreCase("false")) { + shulkerBoxStackSize = 1; + return newValue; + } + if (newValue.equalsIgnoreCase("true")) { + shulkerBoxStackSize = 64; + return newValue; + } + return null; + } + + @Override + public String description() + { + return "Value must either be true, false, or a number between 2-64"; + } + } + + @Rule( + desc = "Empty shulker boxes can stack when thrown on the ground.", + extra = ".. or when manipulated inside the inventories", + validate = StackableShulkerBoxValidator.class, + options = {"false", "true", "16"}, + strict = false, + category = {SURVIVAL, FEATURE} + ) + public static String stackableShulkerBoxes = "false"; + public static int shulkerBoxStackSize = 1; // Referenced from Carpet extra + + @Rule( desc = "Explosions won't destroy blocks", category = {CREATIVE, TNT} ) + public static boolean explosionNoBlockDamage = false; + + @Rule( desc = "Experience will drop from all experience barring blocks with any explosion type", category = {SURVIVAL, FEATURE}) + public static boolean xpFromExplosions = false; + + @Rule( desc = "Removes random TNT momentum when primed", category = {CREATIVE, TNT} ) + public static boolean tntPrimerMomentumRemoved = false; + + @Rule( desc = "TNT causes less lag when exploding in the same spot and in liquids", category = TNT) + public static boolean optimizedTNT = false; + + private static class CheckOptimizedTntEnabledValidator extends Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) { + return optimizedTNT || currentRule.defaultValue().equals(newValue) ? newValue : null; + } + + @Override + public String description() { + return "optimizedTNT must be enabled"; + } + } + + @Rule( desc = "Sets the tnt random explosion range to a fixed value", category = TNT, options = "-1", strict = false, + validate = {CheckOptimizedTntEnabledValidator.class, TNTRandomRangeValidator.class}, extra = "Set to -1 for default behavior") + public static double tntRandomRange = -1; + + private static class TNTRandomRangeValidator extends Validator { + @Override + public Double validate(CommandSourceStack source, CarpetRule currentRule, Double newValue, String string) { + return newValue == -1 || newValue >= 0 ? newValue : null; + } + + @Override + public String description() { + return "Cannot be negative, except for -1"; + } + } + + @Rule( desc = "Sets the horizontal random angle on TNT for debugging of TNT contraptions", category = TNT, options = "-1", strict = false, + validate = TNTAngleValidator.class, extra = "Set to -1 for default behavior") + public static double hardcodeTNTangle = -1.0D; + + private static class TNTAngleValidator extends Validator { + @Override + public Double validate(CommandSourceStack source, CarpetRule currentRule, Double newValue, String string) { + return (newValue >= 0 && newValue < Math.PI * 2) || newValue == -1 ? newValue : null; + } + + @Override + public String description() { + return "Must be between 0 and 2pi, or -1"; + } + } + + @Rule( desc = "Merges stationary primed TNT entities", category = TNT ) + public static boolean mergeTNT = false; + + @Rule( + desc = "Lag optimizations for redstone dust", + extra = { + "by Theosib", + ".. also fixes some locational behaviours or vanilla redstone MC-11193", + "so behaviour of locational vanilla contraptions is not guaranteed" + }, + category = {EXPERIMENTAL, OPTIMIZATION} + ) + public static boolean fastRedstoneDust = false; + + @Rule(desc = "Only husks spawn in desert temples", category = FEATURE) + public static boolean huskSpawningInTemples = false; + + @Rule( desc = "Shulkers will respawn in end cities", category = FEATURE ) + public static boolean shulkerSpawningInEndCities = false; + + @Rule( + desc = "Piglins will respawn in bastion remnants", + extra = "Includes piglins, brutes, and a few hoglins", + category = FEATURE + ) + public static boolean piglinsSpawningInBastions = false; + + @Rule( desc = "TNT doesn't update when placed against a power source", category = {CREATIVE, TNT} ) + public static boolean tntDoNotUpdate = false; + + @Rule( + desc = "Prevents players from rubberbanding when moving too fast", + extra = {"... or being kicked out for 'flying'", + "Puts more trust in clients positioning", + "Increases player allowed mining distance to 32 blocks" + }, + category = {CREATIVE, SURVIVAL} + ) + public static boolean antiCheatDisabled = false; + + private static class QuasiConnectivityValidator extends Validator { + + @Override + public Integer validate(CommandSourceStack source, CarpetRule changingRule, Integer newValue, String userInput) { + int minRange = 0; + int maxRange = 1; + + if (source == null || !source.getServer().isReady()) { + maxRange = Integer.MAX_VALUE; + } else { + for (Level level : source.getServer().getAllLevels()) { + maxRange = Math.max(maxRange, level.getHeight() - 1); + } + } + + return (newValue >= minRange && newValue <= maxRange) ? newValue : null; + } + } + + @Rule( + desc = "Pistons, droppers, and dispensers check for power to the block(s) above them.", + extra = { "Defines the range at which pistons, droppers, and dispensers check for 'quasi power'." }, + category = CREATIVE, + validate = QuasiConnectivityValidator.class + ) + public static int quasiConnectivity = 1; + + @Rule( + desc = "Players can flip and rotate blocks when holding cactus", + extra = { + "Doesn't cause block updates when rotated/flipped", + "Applies to pistons, observers, droppers, repeaters, stairs, glazed terracotta etc..." + }, + category = {CREATIVE, SURVIVAL, FEATURE} + ) + public static boolean flippinCactus = false; + + @Rule( + desc = "hoppers pointing to wool will count items passing through them", + extra = { + "Enables /counter command, and actions while placing red and green carpets on wool blocks", + "Use /counter reset to reset the counter, and /counter to query", + "In survival, place green carpet on same color wool to query, red to reset the counters", + "Counters are global and shared between players, 16 channels available", + "Items counted are destroyed, count up to one stack per tick per hopper" + }, + category = {COMMAND, CREATIVE, FEATURE} + ) + public static boolean hopperCounters = false; + + @Rule( + desc = "Allows Budding Amethyst blocks to be moved", + extra = { + "Allow for them to be moved by pistons", + "as well as adds extra drop when mining with silk touch pickaxe" + }, + category = FEATURE + ) + public static boolean movableAmethyst = false; + + @Rule( desc = "Guardians turn into Elder Guardian when struck by lightning", category = FEATURE ) + public static boolean renewableSponges = false; + + @Rule( desc = "Pistons can push block entities, like hoppers, chests etc.", category = {EXPERIMENTAL, FEATURE} ) + public static boolean movableBlockEntities = false; + + public enum ChainStoneMode { + TRUE, FALSE, STICK_TO_ALL; + public boolean enabled() { + return this != FALSE; + } + } + + @Rule( + desc = "Chains will stick to each other on the long ends", + extra = { + "and will stick to other blocks that connect to them directly.", + "With stick_to_all: it will stick even if not visually connected" + }, + category = {EXPERIMENTAL, FEATURE} + ) + public static ChainStoneMode chainStone = ChainStoneMode.FALSE; + + @Rule( desc = "Saplings turn into dead shrubs in hot climates and no water access", category = FEATURE ) + public static boolean desertShrubs = false; + + @Rule( desc = "Silverfish drop a gravel item when breaking out of a block", category = FEATURE ) + public static boolean silverFishDropGravel = false; + + @Rule( desc = "summoning a lightning bolt has all the side effects of natural lightning", category = CREATIVE ) + public static boolean summonNaturalLightning = false; + + @Rule(desc = "Enables /spawn command for spawn tracking", category = COMMAND) + public static String commandSpawn = "ops"; + + @Rule(desc = "Enables /tick command to control game clocks", category = COMMAND) + public static String commandTick = "ops"; + + @Rule( + desc = "Enables /profile command to monitor game performance", + extra = "subset of /tick command capabilities", + category = COMMAND + ) + public static String commandProfile = "true"; + + @Rule( + desc = "Required permission level for /perf command", + options = {"2", "4"}, + category = CREATIVE + ) + public static int perfPermissionLevel = 4; + + @Rule(desc = "Enables /log command to monitor events via chat and overlays", category = COMMAND) + public static String commandLog = "true"; + + @Rule( + desc = "sets these loggers in their default configurations for all new players", + extra = "use csv, like 'tps,mobcaps' for multiple loggers, none for nothing", + category = {CREATIVE, SURVIVAL}, + options = {"none", "tps", "mobcaps,tps"}, + strict = false + ) + public static String defaultLoggers = "none"; + + @Rule( + desc = "Enables /distance command to measure in game distance between points", + extra = "Also enables brown carpet placement action if 'carpets' rule is turned on as well", + category = COMMAND + ) + public static String commandDistance = "true"; + + @Rule( + desc = "Enables /info command for blocks", + extra = { + "Also enables gray carpet placement action", + "if 'carpets' rule is turned on as well" + }, + category = COMMAND + ) + public static String commandInfo = "true"; + + @Rule( + desc = "Enables /perimeterinfo command", + extra = "... that scans the area around the block for potential spawnable spots", + category = COMMAND + ) + public static String commandPerimeterInfo = "true"; + + @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 = "ops"; + + + @Rule( + desc = "Enables /script command", + extra = "An in-game scripting API for Scarpet programming language", + category = {COMMAND, SCARPET} + ) + public static String commandScript = "true"; + + private static class ModulePermissionLevel extends Validator { + @Override public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String string) { + int permissionLevel = switch (newValue) { + case "false" -> 0; + case "true", "ops" -> 2; + case "0", "1", "2", "3", "4" -> Integer.parseInt(newValue); + default -> throw new IllegalArgumentException(); // already checked by previous validator + }; + if (source != null && !source.hasPermission(permissionLevel)) + return null; + CarpetSettings.runPermissionLevel = permissionLevel; + if (source != null) + CommandHelper.notifyPlayersCommandsChanged(source.getServer()); + return newValue; + } + @Override + 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.", + "It is also the permission level apps will", + "have when running commands with run()" + }, + category = {SCARPET}, + options = {"ops", "0", "1", "2", "3", "4"}, + validate = {Validators.CommandLevel.class, ModulePermissionLevel.class} + ) + public static String commandScriptACE = "ops"; + + @Rule( + desc = "Scarpet script from world files will autoload on server/world start ", + extra = "if /script is enabled", + category = SCARPET + ) + public static boolean scriptsAutoload = true; + + @Rule( + desc = "Enables scripts debugging messages in system log", + category = SCARPET + ) + public static boolean scriptsDebugging = false; + + @Rule( + desc = "Enables scripts optimization", + category = SCARPET + ) + public static boolean scriptsOptimization = true; + + private static class ScarpetAppStore extends Validator { + @Override + public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String stringInput) { + if (newValue.equals(currentRule.value())) { + // Don't refresh the local repo if it's the same (world change), helps preventing hitting rate limits from github when + // getting suggestions. Pending is a way to invalidate the cache when it gets old, and investigating api usage further + return newValue; + } + if (newValue.equals("none")) { + AppStoreManager.setScarpetRepoLink(null); + } else { + if (newValue.endsWith("/")) + newValue = newValue.substring(0, newValue.length() - 1); + AppStoreManager.setScarpetRepoLink("https://api.github.com/repos/" + newValue + "/"); + } + if (source != null) + CommandHelper.notifyPlayersCommandsChanged(source.getServer()); + return newValue; + } + + @Override + public String description() { + return "Appstore link should point to a valid github repository"; + } + } + + @Rule( + desc = "Location of the online repository of scarpet apps", + extra = { + "set to 'none' to disable.", + "Point to any github repo with scarpet apps", + "using //contents/" + }, + category = SCARPET, + strict = false, + validate = ScarpetAppStore.class + ) + public static String scriptsAppStore = "gnembon/scarpet/contents/programs"; + + + @Rule(desc = "Enables /player command to control/spawn players", category = COMMAND) + public static String commandPlayer = "ops"; + + @Rule(desc = "Spawn offline players in online mode if online-mode player with specified name does not exist", category = COMMAND) + public static boolean allowSpawningOfflinePlayers = true; + + @Rule(desc = "Allows listing fake players on the multiplayer screen", category = COMMAND) + public static boolean allowListingFakePlayers = false; + + @Rule(desc = "Allows to track mobs AI via /track command", category = COMMAND) + public static String commandTrackAI = "ops"; + + @Rule(desc = "Placing carpets may issue carpet commands for non-op players", category = SURVIVAL) + public static boolean carpets = false; + + @Rule( + desc = "Glass can be broken faster with pickaxes", + category = SURVIVAL + ) + public static boolean missingTools = false; + + @Rule(desc = "fill/clone/setblock and structure blocks cause block updates", category = CREATIVE) + public static boolean fillUpdates = true; + + @Rule(desc = "placing blocks cause block updates", category = CREATIVE) + public static boolean interactionUpdates = true; + + @Rule(desc = "Disables breaking of blocks caused by flowing liquids", category = CREATIVE) + public static boolean liquidDamageDisabled = false; + + + @Rule( + desc = "smooth client animations with low tps settings", + extra = "works only in SP, and will slow down players", + category = {CREATIVE, SURVIVAL, CLIENT} + ) + public static boolean smoothClientAnimations = true; + + private static class PushLimitLimits extends Validator { + @Override public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) { + return (newValue>0 && newValue <= 1024) ? newValue : null; + } + @Override + public String description() { return "You must choose a value from 1 to 1024";} + } + @Rule( + desc = "Customizable piston push limit", + options = {"10", "12", "14", "100"}, + category = CREATIVE, + strict = false, + validate = PushLimitLimits.class + ) + public static int pushLimit = PistonStructureResolver.MAX_PUSH_DEPTH; + + @Rule( + desc = "Customizable powered rail power range", + options = {"9", "15", "30"}, + category = CREATIVE, + strict = false, + validate = PushLimitLimits.class + ) + public static int railPowerLimit = 9; + + private static class ForceloadLimitValidator extends Validator + { + @Override + public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) + { + return (newValue > 0 && newValue <= 20000000) ? newValue : null; + } + + @Override + public String description() { return "You must choose a value from 1 to 20M";} + } + @Rule( + desc = "Customizable forceload chunk limit", + options = {"256"}, + category = CREATIVE, + strict = false, + validate = ForceloadLimitValidator.class + ) + public static int forceloadLimit = 256; + + @Rule( + desc = "Customizable maximal entity collision limits, 0 for no limits", + options = {"0", "1", "20"}, + category = OPTIMIZATION, + strict = false, + validate = Validators.NonNegativeNumber.class + ) + 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 = Validators.NonNegativeNumber.class + ) + public static int pingPlayerListLimit = 12; + /* + + @Rule( + desc = "fixes water performance issues", + category = OPTIMIZATION, + validate = Validator.WIP.class + ) + public static boolean waterFlow = true; + */ + + @Rule( + desc = "Sets a different motd message on client trying to connect to the server", + extra = "use '_' to use the startup setting from server.properties", + options = "_", + strict = false, + category = CREATIVE + ) + public static String customMOTD = "_"; + + @Rule( + desc = "Cactus in dispensers rotates blocks.", + extra = "Rotates block anti-clockwise if possible", + category = {FEATURE, DISPENSER} + ) + public static boolean rotatorBlock = false; + + private static class ViewDistanceValidator extends Validator + { + @Override public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) + { + if (currentRule.value().equals(newValue) || source == null) + { + return newValue; + } + if (newValue < 0 || newValue > 32) + { + Messenger.m(source, "r view distance has to be between 0 and 32"); + return null; + } + MinecraftServer server = source.getServer(); + + if (server.isDedicatedServer()) + { + int vd = (newValue >= 2)?newValue:((ServerInterface) server).getProperties().viewDistance; + if (vd != server.getPlayerList().getViewDistance()) + server.getPlayerList().setViewDistance(vd); + return newValue; + } + else + { + Messenger.m(source, "r view distance can only be changed on a server"); + return 0; + } + } + @Override + public String description() { return "You must choose a value from 0 (use server settings) to 32";} + } + @Rule( + desc = "Changes the view distance of the server.", + extra = "Set to 0 to not override the value in server settings.", + options = {"0", "12", "16", "32"}, + category = CREATIVE, + strict = false, + validate = ViewDistanceValidator.class + ) + public static int viewDistance = 0; + + private static class SimulationDistanceValidator extends Validator + { + @Override public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) + { + if (currentRule.value().equals(newValue) || source == null) + { + return newValue; + } + if (newValue < 0 || newValue > 32) + { + Messenger.m(source, "r simulation distance has to be between 0 and 32"); + return null; + } + MinecraftServer server = source.getServer(); + + if (server.isDedicatedServer()) + { + int vd = (newValue >= 2)?newValue:((DedicatedServer) server).getProperties().simulationDistance; + if (vd != server.getPlayerList().getSimulationDistance()) + server.getPlayerList().setSimulationDistance(vd); + return newValue; + } + else + { + Messenger.m(source, "r simulation distance can only be changed on a server"); + return 0; + } + } + @Override + public String description() { return "You must choose a value from 0 (use server settings) to 32";} + } + @Rule( + desc = "Changes the simulation distance of the server.", + extra = "Set to 0 to not override the value in server settings.", + options = {"0", "12", "16", "32"}, + category = CREATIVE, + strict = false, + validate = SimulationDistanceValidator.class + ) + public static int simulationDistance = 0; + + public enum RenewableCoralMode { + FALSE, + EXPANDED, + TRUE; + } + @Rule( + desc = "Coral structures will grow with bonemeal from coral plants", + extra = "Expanded also allows growing from coral fans for sustainable farming outside of warm oceans", + category = FEATURE + ) + public static RenewableCoralMode renewableCoral = RenewableCoralMode.FALSE; + + @Rule( + desc = "Nether basalt generator without soul sand below ", + extra = " .. will convert into blackstone instead", + category = FEATURE + ) + public static boolean renewableBlackstone = false; + + @Rule( + desc = "Lava and water generate deepslate and cobbled deepslate instead below Y0", + category = FEATURE + ) + public static boolean renewableDeepslate = false; + + @Rule(desc = "fixes block placement rotation issue when player rotates quickly while placing blocks", category = RuleCategory.BUGFIX) + public static boolean placementRotationFix = false; + + @Rule(desc = "Spawning requires much less CPU and Memory", category = OPTIMIZATION) + public static boolean lagFreeSpawning = false; + + @Rule( + desc = "Increases for testing purposes number of blue skulls shot by the wither", + category = CREATIVE + ) + public static boolean moreBlueSkulls = 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; + public static boolean isCreativeFlying(Entity entity) + { + // #todo replace after merger to 1.17 + return CarpetSettings.creativeNoClip && entity instanceof Player && (((Player) entity).isCreative()) && ((Player) entity).getAbilities().flying; + } + + + @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}, + strict = false, + validate = Validators.NonNegativeNumber.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}, + strict = false, + validate = Validators.Probablity.class + ) + public static double creativeFlyDrag = 0.09; + + @Rule( + desc = "Removes obnoxious messages from the logs", + extra = { + "Doesn't display 'Maximum sound pool size 247 reached'", + "Which is normal with decent farms and contraptions" + }, + category = {SURVIVAL, CLIENT} + ) + public static boolean cleanLogs = false; + + public static class StructureBlockLimitValidator extends Validator { + + @Override public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) { + return (newValue >= StructureBlockEntity.MAX_SIZE_PER_AXIS) ? newValue : null; + } + + @Override + public String description() { + return "You have to choose a value greater or equal to 48"; + } + } + @Rule( + desc = "Customizable structure block limit of each axis", + extra = {"WARNING: Needs to be permanent for correct loading.", + "Setting 'structureBlockIgnored' to air is recommended", + "when saving massive structures.", + "Required on client of player editing the Structure Block.", + "'structureBlockOutlineDistance' may be required for", + "correct rendering of long structures."}, + options = {"48", "96", "192", "256"}, + category = CREATIVE, + validate = StructureBlockLimitValidator.class, + strict = false + ) + public static int structureBlockLimit = StructureBlockEntity.MAX_SIZE_PER_AXIS; + + public static class StructureBlockIgnoredValidator extends Validator { + + @Override + public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String string) { + if (source == null) return newValue; // closing or sync + Optional ignoredBlock = source.registryAccess().registryOrThrow(Registries.BLOCK).getOptional(ResourceLocation.tryParse(newValue)); + if (!ignoredBlock.isPresent()) { + Messenger.m(source, "r Unknown block '" + newValue + "'."); + return null; + } + structureBlockIgnoredBlock = ignoredBlock.get(); + return newValue; + } + } + @Rule( + desc = "Changes the block ignored by the Structure Block", + options = {"minecraft:structure_void", "minecraft:air"}, + category = CREATIVE, + validate = StructureBlockIgnoredValidator.class, + strict = false + ) + public static String structureBlockIgnored = "minecraft:structure_void"; + + @Rule( + desc = "Customizable Structure Block outline render distance", + extra = "Required on client to work properly", + options = {"96", "192", "2048"}, + category = {CREATIVE, CLIENT}, + strict = false, + validate = Validators.NonNegativeNumber.class + ) + public static int structureBlockOutlineDistance = 96; + + @Rule( + desc = "Lightning kills the items that drop when lightning kills an entity", + 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; + + @Rule( + desc = "Placing an activator rail on top of a barrier block will fill the neighbor updater stack when the rail turns off.", + extra = {"The integer entered is the amount of updates that should be left in the stack", "-1 turns it off"}, + category = CREATIVE, + options = {"-1","0","10","50"}, + strict = false, + validate = UpdateSuppressionBlockModes.class + ) + public static int updateSuppressionBlock = -1; + + private static class UpdateSuppressionBlockModes extends Validator { + @Override + public Integer validate(CommandSourceStack source, CarpetRule currentRule, Integer newValue, String string) { + return newValue < -1 ? null : newValue; + } + @Override + public String description() { + return "This value represents the amount of updates required before the logger logs them. Must be -1 or larger"; + } + } + + @Rule( + desc = "Creative players load chunks, or they don't! Just like spectators!", + extra = {"Toggling behaves exactly as if the player is in spectator mode and toggling the gamerule spectatorsGenerateChunks." + }, + category = {CREATIVE, FEATURE} + ) + public static boolean creativePlayersLoadChunks = true; + + @Rule( + desc = "Customizable sculk sensor range", + options = {"8", "16", "32"}, + category = CREATIVE, + strict = false, + validate = PushLimitLimits.class + ) + public static int sculkSensorRange = 8; + + /** + * Listener, we need to update world borders to change whether + * they are currently moving in real time or in game time. + */ + private static class WorldBorderValidator extends Validator + { + @Override + public Boolean validate(CommandSourceStack source, CarpetRule changingRule, Boolean newValue, String userInput) + { + if (changingRule.value() ^ newValue) + { + // Needed for the update + tickSyncedWorldBorders = newValue; + MinecraftServer server = CarpetServer.minecraft_server; + if (server == null) + { + return newValue; + } + for (ServerLevel level : server.getAllLevels()) + { + WorldBorder worldBorder = level.getWorldBorder(); + if (worldBorder.getStatus() != BorderStatus.STATIONARY) + { + double from = worldBorder.getSize(); + double to = worldBorder.getLerpTarget(); + long time = worldBorder.getLerpRemainingTime(); + worldBorder.lerpSizeBetween(from, to, time); + } + } + } + return newValue; + } + } + + @Rule( + desc = "Makes world borders move based on in game time instead of real time", + extra = "This has the effect that when the tick rate changes the world border speed also changes proportional to it", + category = FEATURE, + validate = WorldBorderValidator.class + ) + public static boolean tickSyncedWorldBorders = false; + + public enum FungusGrowthMode { + FALSE, RANDOM, ALL; + } + + // refers to "[MC-215169](https://bugs.mojang.com/browse/MC-215169)." - unconfirmed yet that its a java bug + @Rule( + desc = "Allows to grow nether fungi with 3x3 base with bonemeal", + extra = {"Setting to 'all' will make all nether fungi grow into 3x3 trees", "Setting to 'random' will make 6% of all nether fungi grow into 3x3 trees", "(this being consistent with worldgen)"}, + category = {SURVIVAL, FEATURE} + ) + public static FungusGrowthMode thickFungusGrowth = FungusGrowthMode.FALSE; +} diff --git a/src/main/java/carpet/api/settings/CarpetRule.java b/src/main/java/carpet/api/settings/CarpetRule.java new file mode 100644 index 0000000..70edf7a --- /dev/null +++ b/src/main/java/carpet/api/settings/CarpetRule.java @@ -0,0 +1,126 @@ +package carpet.api.settings; + +import java.util.Collection; +import java.util.List; + +import org.apache.commons.lang3.ClassUtils; + +import carpet.network.ServerNetworkHandler; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; + +/** + *

A Carpet rule, that can return its required properties and stores a value.

+ * + *

Name and description translations are picked up from the translation system.

+ * + * @param The value's type + */ +public interface CarpetRule { + /** + *

Returns this rule's name

+ * + *

This is also the rule's id, used as its key in {@link SettingsManager}, for changing rules in-game and for creation + * of translation keys for the rule.

+ * + *

This method must always return the same value for the same {@link CarpetRule}.

+ */ + String name(); + + /** + *

Returns a {@link List} of {@link BaseComponent} with extra information about this rule, that is, + * the lines after the rule's description.

+ * + *

Handling of translation of the result of this method is responsibility of the rule implementation.

+ */ + List extraInfo(); + + /** + *

Returns a {@link Collection} of categories this rule is on.

+ */ + Collection categories(); + + /** + *

Returns a {@link Collection} of suggestions for values that this rule will be able + * to accept as {@link String strings}. The rule must be able to accept all of the suggestions in the returned {@link Collection} as a value, + * though it may have requirements for those to be applicable.

+ * + *

The returned collection must contain the rule's default value.

+ */ + Collection suggestions(); + + /** + *

Returns the {@link SettingsManager} this rule is in.

+ * + *

This method may be removed or changed in the future, and is not part of the api contract, but it's needed to sync + * the rule with clients and other workings. Overriding this method should be safe.

+ */ + SettingsManager settingsManager(); + + /** + *

Returns this rule's value

+ */ + T value(); + + /** + *

Returns whether this rule can be toggled in the client-side when not connected to a + * Carpet server.

+ * + *

In the default implementation, this is the case when {@link #categories()} contains {@link RuleCategory#CLIENT}

+ */ + boolean canBeToggledClientSide(); + + /** + *

Returns the type of this rule's value.

+ * + *

If this rule's type is primitive, it returns a wrapped version of it (such as the result of running + * {@link ClassUtils#primitiveToWrapper(Class)} on it)

+ */ + Class type(); + + /** + *

Returns this rule's default value.

+ * + *

This value will never be {@code null}, and will always be a valid value for {@link #set(CommandSourceStack, Object)}.

+ */ + T defaultValue(); + + /** + *

Returns whether this rule is strict.

+ * + *

A rule being strict means that it will only accept the suggestions returned by {@link #suggestions()} as valid values.

+ * + *

Note that a rule implementation may return {@code false} in this method but still not accept options other than those + * returned in {@link #suggestions()}, only the opposite is guaranteed.

+ */ + default boolean strict() { + return false; + } + + /** + *

Sets this rule's value to the provided {@link String}, after first converting the {@link String} into a suitable type.

+ * + *

This methods run any required validation on the value first, and throws {@link InvalidRuleValueException} if the value is not suitable for + * this rule, regardless of whether it was impossible to convert the value to the required type, the rule doesn't accept the value, or the rule is + * immutable.

+ * + *

Implementations of this method must notify their {@link SettingsManager} by calling + * {@link SettingsManager#notifyRuleChanged(CommandSourceStack, CarpetRule, String)}, who is responsible for notifying the + * {@link ServerNetworkHandler} (if the rule isn't restricted from being synchronized with clients) and other rule observers like the Scarpet + * event, in case the value of the rule was changed because of the invocation.

+ * + *

This method must not throw any exception other than the documented {@link InvalidRuleValueException}.

+ * + * @param source The {@link CommandSourceStack} to notify about the result of this rule change or {@code null} in order to not notify + * @param value The new value for this rule as a {@link String} + * @throws InvalidRuleValueException if the value passed to the method was not valid as a value to this rule, either because of incompatible type, + * because the rule can't accept that value or because there was some requirement missing for that value to be allowed + */ + void set(CommandSourceStack source, String value) throws InvalidRuleValueException; + + /** + *

This method follows the same contract as {@link #set(CommandSourceStack, String)}, but accepts a value already parsed (though not verified).

+ * @see #set(CommandSourceStack, String) + */ + void set(CommandSourceStack source, T value) throws InvalidRuleValueException; +} diff --git a/src/main/java/carpet/api/settings/InvalidRuleValueException.java b/src/main/java/carpet/api/settings/InvalidRuleValueException.java new file mode 100644 index 0000000..82e04b3 --- /dev/null +++ b/src/main/java/carpet/api/settings/InvalidRuleValueException.java @@ -0,0 +1,36 @@ +package carpet.api.settings; + +import carpet.utils.Messenger; +import net.minecraft.commands.CommandSourceStack; + +/** + *

An {@link Exception} thrown when the value given for a {@link CarpetRule} is invalid.

+ * + *

It can hold a message to be sent to the executing source.

+ */ +public class InvalidRuleValueException extends Exception { + + /** + *

Constructs a new {@link InvalidRuleValueException} with a message that will be passed to the executing source

+ * @param cause The cause of the exception + */ + public InvalidRuleValueException(String cause) { + super(cause); + } + + /** + *

Constructs a new {@link InvalidRuleValueException} with no detail message, that therefore should not notify the source

+ */ + public InvalidRuleValueException() { + super(); + } + + /** + *

Notifies the given source with the exception's message if it exists, does nothing if it doesn't exist or it is {@code null}

+ * @param source The source to notify + */ + public void notifySource(String ruleName, CommandSourceStack source) { + if (getMessage() != null) + Messenger.m(source, "r Couldn't set value for rule " + ruleName + ": "+ getMessage()); + } +} diff --git a/src/main/java/carpet/api/settings/Rule.java b/src/main/java/carpet/api/settings/Rule.java new file mode 100644 index 0000000..f899140 --- /dev/null +++ b/src/main/java/carpet/api/settings/Rule.java @@ -0,0 +1,82 @@ +package carpet.api.settings; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Any field in this class annotated with this class is interpreted as a carpet rule. + * The field must be static and have a type of one of: + * - boolean + * - int + * - double + * - String + * - long + * - float + * - a subclass of Enum + * The default value of the rule will be the initial value of the field. + */ +@Documented +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Rule +{ + /** + *

An array of categories the rule is in, as {@link String strings}.

+ * + *

Those must have a corresponding translation key. Categories provided in Carpet's {@link RuleCategory} already + * have translation key.

+ */ + String[] categories(); + + /** + * Options to select in menu. + * Inferred for booleans and enums. Otherwise, must be present. + */ + String[] options() default {}; + + /** + * if a rule is not strict - can take any value, otherwise it needs to match + * any of the options + * For enums, its always strict, same for booleans - no need to set that for them. + */ + boolean strict() default true; + + /** + * If specified, the rule will automatically enable or disable + * a builtin Scarpet Rule App with this name. + */ + String appSource() default ""; + + /** + * The class of the validator checked right before the rule is changed, using the returned value as the new value to set, or cancel the change if null is returned. + */ + @SuppressWarnings("rawtypes") + Class[] validators() default {}; + + /** + *

A class or list of classes implementing {@link Condition} that will have their {@link Condition#shouldRegister()} method + * executed while the rule is being parsed in {@link SettingsManager#parseSettingsClass(Class)}, that will cause the rule to be skipped + * if it evaluates to false

+ * + * @see Condition + */ + Class[] conditions() default {}; + + /** + *

Represents a condition that must be met for a rule to be registered in a {@link SettingsManager} via + * {@link SettingsManager#parseSettingsClass(Class)}

+ * + * @see Rule#conditions() + * @see #shouldRegister() + */ + interface Condition { + /** + *

Returns whether the rule that has had this {@link Condition} added should register.

+ * @return {@code true} to register the rule, {@code false} otherwise + */ + boolean shouldRegister(); + } +} diff --git a/src/main/java/carpet/api/settings/RuleCategory.java b/src/main/java/carpet/api/settings/RuleCategory.java new file mode 100644 index 0000000..0f785ca --- /dev/null +++ b/src/main/java/carpet/api/settings/RuleCategory.java @@ -0,0 +1,29 @@ +package carpet.api.settings; + +/** + *

Multiple categories for rules that Carpet uses and can be used to group other rules under the same + * categories.

+ * + *

These are not the only categories that can be used, you can create your own ones for your rules + * and they will be added to the SettingsManager.

+ * + */ +public class RuleCategory { + public static final String BUGFIX = "bugfix"; + public static final String SURVIVAL = "survival"; + public static final String CREATIVE = "creative"; + public static final String EXPERIMENTAL = "experimental"; + public static final String OPTIMIZATION = "optimization"; + public static final String FEATURE = "feature"; + public static final String COMMAND = "command"; + public static final String TNT = "tnt"; + public static final String DISPENSER = "dispenser"; + public static final String SCARPET = "scarpet"; + /** + * Rules with this {@link RuleCategory} will have a client-side + * counterpart, so they can be set independently without the server + * running Carpet + */ + public static final String CLIENT = "client"; + +} diff --git a/src/main/java/carpet/api/settings/RuleHelper.java b/src/main/java/carpet/api/settings/RuleHelper.java new file mode 100644 index 0000000..8966df3 --- /dev/null +++ b/src/main/java/carpet/api/settings/RuleHelper.java @@ -0,0 +1,107 @@ +package carpet.api.settings; + +import java.util.Locale; + +import carpet.utils.TranslationKeys; +import carpet.utils.Translations; +import net.minecraft.commands.CommandSourceStack; + +/** + *

A helper class for operating with {@link CarpetRule} instances and values.

+ * + *

If a method is visible and has javadocs it's probably API

+ */ +public final class RuleHelper { + private RuleHelper() {} + + /** + *

Gets a {@code boolean} value for a given {@link CarpetRule}.

+ * + *

The current implementation is as follows:

+ *
    + *
  • If the rule's type is a {@code boolean}, it will return the value directly.
  • + *
  • If the rule's type is a {@link Number}, it will return {@code true} if the value is greater than zero.
  • + *
  • In any other case, it will return {@code false}.
  • + *
+ * @param rule The rule to get the {@code boolean} value from + * @return A {@code boolean} representation of this rule's value + */ + public static boolean getBooleanValue(CarpetRule rule) { + if (rule.type() == Boolean.class) return (boolean) rule.value(); + if (Number.class.isAssignableFrom(rule.type())) return ((Number) rule.value()).doubleValue() > 0; + return false; + } + + /** + *

Converts a rule's value into its similar {@link String} representation.

+ * + *

If the value is an {@link Enum}, this method returns the name of the enum constant lowercased, else + * it returns the result of running {@link #toString()} on the value.

+ * @param value A rule's value + * @return A {@link String} representation of the given value + */ + public static String toRuleString(Object value) { + if (value instanceof Enum) return ((Enum) value).name().toLowerCase(Locale.ROOT); + return value.toString(); + } + + /** + *

Checks whether the given {@code CarpetRule rule} is in its default value

+ * + * @param rule The rule to check + * @return {@code true} if the rule's default value equals its current value + */ + public static boolean isInDefaultValue(CarpetRule rule) { + return rule.defaultValue().equals(rule.value()); + } + + /** + *

Resets the given {@link CarpetRule rule} to its default value, and notifies the given source, if provided.

+ * @param rule The {@link CarpetRule} to reset to its default value + * @param source A {@link ServerCommandSource} to notify about this change, or {@code null} + * + * @param The type of the {@link CarpetRule} + */ + public static void resetToDefault(CarpetRule rule, CommandSourceStack source) { + try { + rule.set(source, rule.defaultValue()); + } catch (InvalidRuleValueException e) { + throw new IllegalStateException("Rule couldn't be set to default value!", e); + } + } + + // Translations + // The methods from this point are not stable API yet and may change or be removed in binary incompatible ways later + + /** + * @param rule The {@link CarpetRule} to get the translated name of + * @return A {@link String} being the translated {@link CarpetRule#name() name} of the given rule, the current language. + * + * @apiNote This method isn't stable API yet and may change or be removed in binary incompatible ways in later Carpet versions + */ + public static String translatedName(CarpetRule rule) { + String key = String.format(TranslationKeys.RULE_NAME_PATTERN, rule.settingsManager().identifier(), rule.name()); + return Translations.hasTranslation(key) ? Translations.tr(key) + String.format(" (%s)", rule.name()): rule.name(); + } + + /** + * @param rule The {@link CarpetRule} to get the translated description of + * @return A {@link String} being the translated description of this rule, in Carpet's configured language. + * + * @apiNote This method isn't stable API yet and may change or be removed in binary incompatible ways in later Carpet versions + */ + public static String translatedDescription(CarpetRule rule) { + return Translations.tr(String.format(TranslationKeys.RULE_DESC_PATTERN, rule.settingsManager().identifier(), rule.name())); + } + + /** + * @param manager A settings manager identifier + * @param category A category identifier + * @return The translated name of the category + * + * @apiNote This method isn't stable API yet and may change or be removed in binary incompatible ways in later Carpet versions + */ + public static String translatedCategory(String manager, String category) { + return Translations.tr(TranslationKeys.CATEGORY_PATTERN.formatted(manager, category), category); + } +} diff --git a/src/main/java/carpet/api/settings/SettingsManager.java b/src/main/java/carpet/api/settings/SettingsManager.java new file mode 100644 index 0000000..b8a0430 --- /dev/null +++ b/src/main/java/carpet/api/settings/SettingsManager.java @@ -0,0 +1,788 @@ +package carpet.api.settings; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.collect.Sets; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; + +import carpet.CarpetExtension; +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.api.settings.Rule.Condition; +import carpet.network.ServerNetworkHandler; +import carpet.settings.ParsedRule; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import carpet.utils.TranslationKeys; +import carpet.utils.Translations; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +import static carpet.utils.Translations.tr; +import static java.util.Comparator.comparing; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; + +/** + *

A {@link SettingsManager} is a class that manages {@link CarpetRule carpet rules} in a {@link MinecraftServer}, + * including a command to manage it, and allowing other code to hook into rule changes by using {@link RuleObserver rule observers}.

+ * + *

You can create your own {@link SettingsManager} if you want to have your own command that will be handled the same way as Carpet's + * by using {@link #SettingsManager(String, String, String)} and returning it in your {@link CarpetExtension}'s + * {@link CarpetExtension#extensionSettingsManager()} method.

+ */ +@SuppressWarnings({"deprecation", "removal"}) // remove after removing old system +public class SettingsManager { + private final Map> rules = new HashMap<>(); + private final String version; + private final String identifier; + private final String fancyName; + private boolean locked; + private MinecraftServer server; + private final List observers = new ArrayList<>(); + private static final List staticObservers = new ArrayList<>(); + static record ConfigReadResult(Map ruleMap, boolean locked) {} + + /** + *

Defines a class that can be notified about a {@link CarpetRule} changing.

+ * + * @see #ruleChanged(CommandSourceStack, CarpetRule, String) + * @see SettingsManager#registerRuleObserver(RuleObserver) + * @see SettingsManager#registerGlobalRuleObserver(RuleObserver) + */ + @FunctionalInterface + public static interface RuleObserver { + /** + *

Notifies this {@link RuleObserver} about the change of a {@link CarpetRule}.

+ * + *

When this is called, the {@link CarpetRule} value has already been set.

+ * + * @param source The {@link CommandSourceStack} that likely originated this change, and should be the notified source for further + * messages. Can be {@code null} if there was none and the operation shouldn't send feedback. + * @param changedRule The {@link CarpetRule} that changed. Use {@link CarpetRule#value() changedRule.value()} to get the rule's value, + * and pass it to {@link RuleHelper#toRuleString(Object)} to get the {@link String} version of it + * @param userInput The {@link String} that the user entered when changing the rule, or a best-effort representation of it in case that is + * is not available at the time (such as loading from disk or a rule being changed programmatically). Note that this value + * may not represent the same string as converting the current value to a {@link String} via {@link RuleHelper#toRuleString(Object)}, + * given the rule implementation may have adapted the input into a different value, for example with the use of a {@link Validator} + */ + void ruleChanged(CommandSourceStack source, CarpetRule changedRule, String userInput); + } + + /** + * Creates a new {@link SettingsManager} with the given version, identifier and fancy name + * + * @param version A {@link String} with the mod's version + * @param identifier A {@link String} with the mod's id, will be the command name + * @param fancyName A {@link String} being the mod's fancy name. + */ + public SettingsManager(String version, String identifier, String fancyName) + { + this.version = version; + this.identifier = identifier; + this.fancyName = fancyName; + } + + /** + *

Registers a {@link RuleObserver} to changes in rules from + * this {@link SettingsManager} instance.

+ * + * @see SettingsManager#registerGlobalRuleObserver(RuleObserver) + * + * @param observer A {@link RuleObserver} that will be called with + * the used {@link CommandSourceStack} and the changed + * {@link CarpetRule}. + */ + public void registerRuleObserver(RuleObserver observer) + { + observers.add(observer); + } + + /** + * Registers a {@link RuleObserver} to changes in rules from + * any {@link SettingsManager} instance (unless their implementation disallows it). + * @see SettingsManager#registerRuleObserver(RuleObserver) + * + * @param observer A {@link RuleObserver} that will be called with + * the used {@link CommandSourceStack}, and the changed + * {@link CarpetRule}. + */ + public static void registerGlobalRuleObserver(RuleObserver observer) + { + staticObservers.add(observer); + } + + /** + * @return A {@link String} being this {@link SettingsManager}'s + * identifier, which is also the command name + */ + public String identifier() { + return identifier; + } + + /** + *

Returns whether this {@link SettingsManager} is locked, and any rules in it should therefore not be + * toggleable and its management command should not be available.

+ * @return {@code true} if this {@link SettingsManager} is locked + */ + public boolean locked() { + return locked; + } + + /** + * Adds all annotated fields with the {@link Rule} annotation + * to this {@link SettingsManager} in order to handle them. + * @param settingsClass The class that will be analyzed + */ + public void parseSettingsClass(Class settingsClass) + { + // In the current translation system languages are not loaded this early. Ensure they are loaded + Translations.updateLanguage(); + boolean warned = settingsClass == CarpetSettings.class; // don't warn for ourselves + + nextRule: for (Field field : settingsClass.getDeclaredFields()) + { + Class[] conditions; + Rule newAnnotation = field.getAnnotation(Rule.class); + carpet.settings.Rule oldAnnotation = field.getAnnotation(carpet.settings.Rule.class); + if (newAnnotation != null) { + conditions = newAnnotation.conditions(); + } else if (oldAnnotation != null) { + conditions = oldAnnotation.condition(); + if (!warned) { + CarpetSettings.LOG.warn(""" + Registering outdated rules for settings class '%s'! + This won't be supported in the future and rules won't be registered!""".formatted(settingsClass.getName())); + warned = true; + } + } else { + continue; + } + for (Class condition : conditions) { //Should this be moved to ParsedRule.of? + try + { + Constructor constr = condition.getDeclaredConstructor(); + constr.setAccessible(true); + if (!(constr.newInstance()).shouldRegister()) + continue nextRule; + } + catch (ReflectiveOperationException e) + { + throw new IllegalArgumentException(e); + } + } + CarpetRule parsed = ParsedRule.of(field, this); + rules.put(parsed.name(), parsed); + } + } + + /** + * @return A String {@link Iterable} with all categories + * that the rules in this {@link SettingsManager} have. + * @implNote This method doesn't cache the result, so each call loops through all rules and finds all present categories + */ + public Iterable getCategories() + { + return getCarpetRules().stream().map(CarpetRule::categories).mapMulti(Collection::forEach).collect(Collectors.toSet()); + } + + /** + *

Gets a registered rule in this {@link SettingsManager}.

+ * + * @param name The name of the rule to get + * @return A {@link CarpetRule} with the provided name or {@code null} if none in this {@link SettingsManager} matches + */ + public CarpetRule getCarpetRule(String name) + { + return rules.get(name); + } + + /** + * @return An unmodifiable {@link Collection} of the registered rules in this {@link SettingsManager}. + */ + public Collection> getCarpetRules() + { + return Collections.unmodifiableCollection(rules.values()); + } + + /** + *

Adds a {@link CarpetRule} to this {@link SettingsManager}.

+ * + *

Useful when having different {@link CarpetRule} implementations instead of a class of {@code static}, + * annotated fields.

+ * + * @param rule The {@link CarpetRule} to add + * @throws UnsupportedOperationException If a rule with that name is already present in this {@link SettingsManager} + */ + public void addCarpetRule(CarpetRule rule) { + if (rules.containsKey(rule.name())) + throw new UnsupportedOperationException(fancyName + " settings manager already contains a rule with that name!"); + rules.put(rule.name(), rule); + } + + public void notifyRuleChanged(CommandSourceStack source, CarpetRule rule, String userInput) + { + observers.forEach(observer -> observer.ruleChanged(source, rule, userInput)); + staticObservers.forEach(observer -> observer.ruleChanged(source, rule, userInput)); + ServerNetworkHandler.updateRuleWithConnectedClients(rule); + switchScarpetRuleIfNeeded(source, rule); //TODO move into rule + } + + /** + * Attaches a {@link MinecraftServer} to this {@link SettingsManager}.
+ * This is handled automatically by Carpet and calling it manually is not supported. + * + * @param server The {@link MinecraftServer} instance to be attached + */ + public void attachServer(MinecraftServer server) + { + this.server = server; + loadConfigurationFromConf(); + } + + /** + * Detaches the {@link MinecraftServer} of this {@link SettingsManager} and + * resets its {@link CarpetRule}s to their default values.
+ * This is handled automatically by Carpet and calling it manually is not supported. + */ + public void detachServer() + { + for (CarpetRule rule : rules.values()) RuleHelper.resetToDefault(rule, null); + server = null; + } + + /** + *

Initializes Scarpet rules in this {@link SettingsManager}, if any.

+ *

This is handled automatically by Carpet and calling it is not supported.

+ */ + public void initializeScarpetRules() { //TODO try remove + for (CarpetRule rule : rules.values()) + { + if (rule instanceof ParsedRule pr && !pr.scarpetApp.isEmpty()) { + switchScarpetRuleIfNeeded(server.createCommandSourceStack(), pr); + } + } + } + + /** + * Calling this method is not supported. + */ + public void inspectClientsideCommand(CommandSourceStack source, String string) + { + if (string.startsWith("/" + identifier + " ")) + { + String[] res = string.split("\\s+", 3); + if (res.length == 3) + { + String rule = res[1]; + String strOption = res[2]; + if (rules.containsKey(rule) && rules.get(rule).canBeToggledClientSide()) + { + try { + rules.get(rule).set(source, strOption); + } catch (InvalidRuleValueException e) { + e.notifySource(rule, source); + } + } + } + } + } + + private void switchScarpetRuleIfNeeded(CommandSourceStack source, CarpetRule carpetRule) //TODO remove. This should be handled by the rule + { + if (carpetRule instanceof ParsedRule rule && !rule.scarpetApp.isEmpty() && CarpetServer.scriptServer != null) // null check because we may be in server init + { + if (RuleHelper.getBooleanValue(rule) || (rule.type() == String.class && !rule.value().equals("false"))) + { + CarpetServer.scriptServer.addScriptHost(source, rule.scarpetApp, s -> CommandHelper.canUseCommand(s, rule.value()), false, false, true, null); + } else { + CarpetServer.scriptServer.removeScriptHost(source, rule.scarpetApp, false, true); + } + } + } + + private Path getFile() + { + return server.getWorldPath(LevelResource.ROOT).resolve(identifier + ".conf"); + } + + private Collection> getRulesSorted() + { + return rules.values().stream().sorted(comparing(CarpetRule::name)).toList(); + } + + /** + * Disables all {@link CarpetRule}s with the {@link RuleCategory#COMMAND} category, + * called when the {@link SettingsManager} is {@link #locked}. + */ + private void disableBooleanCommands() + { + for (CarpetRule rule : rules.values()) + { + if (!rule.categories().contains(RuleCategory.COMMAND)) + continue; + try { + if (rule.suggestions().contains("false")) + rule.set(server.createCommandSourceStack(), "false"); + else + CarpetSettings.LOG.warn("Couldn't disable command rule "+ rule.name() + ": it doesn't suggest false as a valid option"); + } catch (InvalidRuleValueException e) { + throw new IllegalStateException(e); // contract of CarpetRule.suggestions() + } + } + } + + private void writeSettingsToConf(ConfigReadResult data) + { + if (locked) + return; + try (BufferedWriter fw = Files.newBufferedWriter(getFile())) + { + for (String key: data.ruleMap().keySet()) + { + fw.write(key + " " + data.ruleMap().get(key)); + fw.newLine(); + } + } + catch (IOException e) + { + CarpetSettings.LOG.error("[CM]: failed write "+identifier+".conf config file", e); + } + } + + private Collection> findStartupOverrides() + { + Set defaults = readSettingsFromConf(getFile()).ruleMap().keySet(); + return rules.values().stream().filter(r -> defaults.contains(r.name())). + sorted(comparing(CarpetRule::name)).toList(); + } + + private Collection> getNonDefault() + { + return rules.values().stream().filter(Predicate.not(RuleHelper::isInDefaultValue)).sorted(comparing(CarpetRule::name)).toList(); + } + + private void loadConfigurationFromConf() + { + for (CarpetRule rule : rules.values()) RuleHelper.resetToDefault(rule, server.createCommandSourceStack()); + ConfigReadResult conf = readSettingsFromConf(getFile()); + locked = false; + if (conf.locked()) + { + CarpetSettings.LOG.info("[CM]: "+fancyName+" features are locked by the administrator"); + disableBooleanCommands(); + } + int loadedCount = 0; + for (String key: conf.ruleMap().keySet()) + { + try + { + rules.get(key).set(server.createCommandSourceStack(), conf.ruleMap().get(key)); + loadedCount++; + } + catch (InvalidRuleValueException exc) + { + CarpetSettings.LOG.error("[CM Error]: Failed to load setting: " + key, exc); + } + } + if (loadedCount > 0) + CarpetSettings.LOG.info("[CM] Loaded " + loadedCount + " settings from " + identifier + ".conf"); + locked = conf.locked(); + } + + + private ConfigReadResult readSettingsFromConf(Path path) + { + try (BufferedReader reader = Files.newBufferedReader(path)) + { + String line = ""; + boolean confLocked = false; + Map result = new HashMap(); + while ((line = reader.readLine()) != null) + { + line = line.replaceAll("[\\r\\n]", ""); + if ("locked".equalsIgnoreCase(line)) + { + confLocked = true; + } + String[] fields = line.split("\\s+",2); + if (fields.length > 1) + { + if (result.isEmpty() && fields[0].startsWith("#") || fields[1].startsWith("#")) + { + continue; + } + if (!rules.containsKey(fields[0])) + { + CarpetSettings.LOG.error("[CM]: "+fancyName+" Setting " + fields[0] + " is not a valid rule - ignoring..."); + continue; + } + result.put(fields[0], fields[1]); + } + } + return new ConfigReadResult(result, confLocked); + } + catch (NoSuchFileException e) + { + if (path.equals(getFile()) && FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) + { + Path defaultsPath = FabricLoader.getInstance().getConfigDir().resolve("carpet/default_"+identifier+".conf"); + try { + if (Files.notExists(defaultsPath)) + { + Files.createDirectories(defaultsPath.getParent()); + Files.createFile(defaultsPath); + try (BufferedWriter fw = Files.newBufferedWriter(defaultsPath)) + { + fw.write("# This is " + fancyName + "'s default configuration file"); + fw.newLine(); + fw.write("# Settings specified here will be used when a world doesn't have a config file, but they will be completely " + + "ignored once the world has one."); + fw.newLine(); + } + } + return readSettingsFromConf(defaultsPath); + } catch (IOException e2) { + CarpetSettings.LOG.error("Exception when loading fallback default config: ", e2); + } + } + return new ConfigReadResult(new HashMap<>(), false); + } + catch (IOException e) + { + CarpetSettings.LOG.error("Exception while loading Carpet rules from config", e); + return new ConfigReadResult(new HashMap<>(), false); + } + } + + private Collection> getRulesMatching(String search) { + String lcSearch = search.toLowerCase(Locale.ROOT); + return rules.values().stream().filter(rule -> + { + if (rule.name().toLowerCase(Locale.ROOT).contains(lcSearch)) return true; // substring match, case insensitive + for (String c : rule.categories()) if (c.equals(search)) return true; // category exactly, case sensitive + return Sets.newHashSet(RuleHelper.translatedDescription(rule).toLowerCase(Locale.ROOT).split("\\W+")).contains(lcSearch); // contains full term in description, but case insensitive + }).sorted(comparing(CarpetRule::name)).toList(); + } + + /** + * A method to pretty print in markdown (useful for Github wiki/readme) the current + * registered rules for a category to the log. Contains the name, description, + * categories, type, defaults, whether or not they are strict, their suggested + * values, etc. + * + * @param ps A {@link PrintStream} to dump the rules to, such as {@link System#out} + * @param category A {@link String} being the category to print, {@link null} to print + * all registered rules. + * @return actually nothing, the int is just there for brigadier + * + * @apiNote While extensions are expected to be able to call this method, binary compatibility isn't + * guaranteed, but this shouldn't be an issue given this is only intended to be ran for doc + * generation (where version is controlled) and it's not expected to be referenced in production anyway + */ + public int dumpAllRulesToStream(PrintStream ps, String category) + { + ps.println("# "+fancyName+" Settings"); + for (CarpetRule rule : new TreeMap<>(rules).values()) + { + if (category != null && !rule.categories().contains(category)) + continue; + ps.println("## " + rule.name()); + ps.println(RuleHelper.translatedDescription(rule)+" "); + for (Component extra : rule.extraInfo()) + ps.println(extra.getString() + " "); + ps.println("* Type: `" + rule.type().getSimpleName() + "` "); + ps.println("* Default value: `" + RuleHelper.toRuleString(rule.defaultValue()) + "` "); + String options = rule.suggestions().stream().map(s -> "`" + s + "`").collect(Collectors.joining(", ")); + if (!options.isEmpty()) ps.println((rule.strict() ? "* Allowed" : "* Suggested") + " options: " + options + " "); + ps.println("* Categories: " + rule.categories().stream().map(s -> "`" + s.toUpperCase(Locale.ROOT) + "`").collect(Collectors.joining(", ")) + " "); + if (rule instanceof ParsedRule) + { + boolean preamble = false; + for (Validator validator : ((ParsedRule) rule).realValidators) + { + if (validator.description() != null) + { + if (!preamble) + { + ps.println("* Additional notes: "); + preamble = true; + } + ps.println(" * "+validator.description()+" "); + } + } + } + ps.println(" "); + } + return 1; + } + + private CarpetRule contextRule(CommandContext ctx) throws CommandSyntaxException + { + String ruleName = StringArgumentType.getString(ctx, "rule"); + CarpetRule rule = getCarpetRule(ruleName); + if (rule == null) + throw new SimpleCommandExceptionType(Messenger.c("rb "+ tr(TranslationKeys.UNKNOWN_RULE) + ": "+ruleName)).create(); + return rule; + } + + static CompletableFuture suggestMatchingContains(Stream stream, SuggestionsBuilder suggestionsBuilder) { + List regularSuggestionList = new ArrayList<>(); + List smartSuggestionList = new ArrayList<>(); + String query = suggestionsBuilder.getRemaining().toLowerCase(Locale.ROOT); + stream.forEach((listItem) -> { + // Regex camelCase Search + var words = Arrays.stream(listItem.split("(? s.toLowerCase(Locale.ROOT)).collect(Collectors.toList()); + var prefixes = new ArrayList(words.size()); + for (int i = 0; i < words.size(); i++) + prefixes.add(String.join("", words.subList(i, words.size()))); + if (prefixes.stream().anyMatch(s -> s.startsWith(query))) { + smartSuggestionList.add(listItem); + } + // Regular prefix matching, reference: CommandSource.suggestMatching + if (SharedSuggestionProvider.matchesSubStr(query, listItem.toLowerCase(Locale.ROOT))) { + regularSuggestionList.add(listItem); + } + }); + var filteredSuggestionList = regularSuggestionList.isEmpty() ? smartSuggestionList : regularSuggestionList; + Objects.requireNonNull(suggestionsBuilder); + filteredSuggestionList.forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + } + + /** + * Registers the settings command for this {@link SettingsManager}.
+ * It is handled automatically by Carpet. + * @param dispatcher The current {@link CommandDispatcher} + * @param commandBuildContext The current {@link CommandBuildContext} + */ + public void registerCommand(final CommandDispatcher dispatcher, final CommandBuildContext commandBuildContext) + { + if (dispatcher.getRoot().getChildren().stream().anyMatch(node -> node.getName().equalsIgnoreCase(identifier))) + { + CarpetSettings.LOG.error("Failed to add settings command for " + identifier + ". It is masking previous command."); + return; + } + + LiteralArgumentBuilder literalargumentbuilder = literal(identifier).requires((player) -> + CommandHelper.canUseCommand(player, CarpetSettings.carpetCommandPermissionLevel) && !locked()); + + literalargumentbuilder.executes((context)-> listAllSettings(context.getSource())). + then(literal("list"). + executes( (c) -> listSettings(c.getSource(), String.format(tr(TranslationKeys.ALL_MOD_SETTINGS), fancyName), + getRulesSorted())). + then(literal("defaults"). + executes( (c)-> listSettings(c.getSource(), + String.format(tr(TranslationKeys.CURRENT_FROM_FILE_HEADER), fancyName, (identifier+".conf")), + findStartupOverrides()))). + then(argument("tag",StringArgumentType.word()). + suggests( (c, b)->suggest(getCategories(), b)). + executes( (c) -> listSettings(c.getSource(), + String.format(tr(TranslationKeys.MOD_SETTINGS_MATCHING), fancyName, RuleHelper.translatedCategory(identifier(),StringArgumentType.getString(c, "tag"))), + getRulesMatching(StringArgumentType.getString(c, "tag")))))). + then(literal("removeDefault"). + requires(s -> !locked()). + then(argument("rule", StringArgumentType.word()). + suggests( (c, b) -> suggestMatchingContains(getRulesSorted().stream().map(CarpetRule::name), b)). + executes((c) -> removeDefault(c.getSource(), contextRule(c))))). + then(literal("setDefault"). + requires(s -> !locked()). + then(argument("rule", StringArgumentType.word()). + suggests( (c, b) -> suggestMatchingContains(getRulesSorted().stream().map(CarpetRule::name), b)). + then(argument("value", StringArgumentType.greedyString()). + suggests((c, b)-> suggest(contextRule(c).suggestions(), b)). + executes((c) -> setDefault(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value")))))). + then(argument("rule", StringArgumentType.word()). + suggests( (c, b) -> suggestMatchingContains(getRulesSorted().stream().map(CarpetRule::name), b)). + requires(s -> !locked() ). + executes( (c) -> displayRuleMenu(c.getSource(), contextRule(c))). + then(argument("value", StringArgumentType.greedyString()). + suggests((c, b)-> suggest(contextRule(c).suggestions(),b)). + executes((c) -> setRule(c.getSource(), contextRule(c), StringArgumentType.getString(c, "value"))))); + + dispatcher.register(literalargumentbuilder); + } + + private int displayRuleMenu(CommandSourceStack source, CarpetRule rule) //TODO check if there's dupe code around options buttons + { + String displayName = RuleHelper.translatedName(rule); + + Messenger.m(source, ""); + Messenger.m(source, "wb "+ displayName ,"!/"+identifier+" "+rule.name(),"^g refresh"); + Messenger.m(source, "w "+ RuleHelper.translatedDescription(rule)); + + rule.extraInfo().forEach(s -> Messenger.m(source, s)); + + List tags = new ArrayList<>(); + tags.add(Messenger.c("w "+ tr(TranslationKeys.TAGS)+": ")); + for (String t: rule.categories()) + { + String translated = RuleHelper.translatedCategory(identifier(), t); + tags.add(Messenger.c("c ["+ translated +"]", "^g "+ String.format(tr(TranslationKeys.LIST_ALL_CATEGORY), translated),"!/"+identifier+" list "+t)); + tags.add(Messenger.c("w , ")); + } + tags.remove(tags.size() - 1); + Messenger.m(source, tags.toArray(new Object[0])); + + Messenger.m(source, "w "+ tr(TranslationKeys.CURRENT_VALUE)+": ", String.format("%s %s (%s value)", RuleHelper.getBooleanValue(rule) ? "lb" : "nb", RuleHelper.toRuleString(rule.value()), RuleHelper.isInDefaultValue(rule) ? "default" : "modified")); + List options = new ArrayList<>(); + options.add(Messenger.c("w Options: ", "y [ ")); + for (String o: rule.suggestions()) + { + options.add(makeSetRuleButton(rule, o, false)); + options.add(Messenger.c("w ")); + } + options.remove(options.size()-1); + options.add(Messenger.c("y ]")); + Messenger.m(source, options.toArray(new Object[0])); + + return 1; + } + + private int setRule(CommandSourceStack source, CarpetRule rule, String newValue) + { + try { + rule.set(source, newValue); + Messenger.m(source, "w "+rule.toString()+", ", "c ["+ tr(TranslationKeys.CHANGE_PERMANENTLY)+"?]", + "^w "+String.format(tr(TranslationKeys.CHANGE_PERMANENTLY_HOVER), identifier+".conf"), + "?/"+identifier+" setDefault "+rule.name()+" "+ RuleHelper.toRuleString(rule.value())); + } catch (InvalidRuleValueException e) { + e.notifySource(rule.name(), source); + } + return 1; + } + + // stores different defaults in the file + private int setDefault(CommandSourceStack source, CarpetRule rule, String stringValue) + { + if (locked()) return 0; + if (!rules.containsKey(rule.name())) return 0; + ConfigReadResult conf = readSettingsFromConf(getFile()); + conf.ruleMap().put(rule.name(), stringValue); + writeSettingsToConf(conf); // this may feels weird, but if conf + // is locked, it will never reach this point. + try { + rule.set(source, stringValue); + Messenger.m(source ,"gi "+String.format(tr(TranslationKeys.DEFAULT_SET), RuleHelper.translatedName(rule), stringValue)); + } catch (InvalidRuleValueException e) { + e.notifySource(rule.name(), source); + } + return 1; + } + // removes overrides of the default values in the file + private int removeDefault(CommandSourceStack source, CarpetRule rule) + { + if (locked) return 0; + if (!rules.containsKey(rule.name())) return 0; + ConfigReadResult conf = readSettingsFromConf(getFile()); + conf.ruleMap().remove(rule.name()); + writeSettingsToConf(conf); + RuleHelper.resetToDefault(rules.get(rule.name()), source); + Messenger.m(source ,"gi "+String.format(tr(TranslationKeys.DEFAULT_REMOVED), RuleHelper.translatedName(rule))); + return 1; + } + + private Component displayInteractiveSetting(CarpetRule rule) + { + String displayName = RuleHelper.translatedName(rule); + List args = new ArrayList<>(); + args.add("w - "+ displayName +" "); + args.add("!/"+identifier+" "+rule.name()); + args.add("^y "+RuleHelper.translatedDescription(rule)); + for (String option: rule.suggestions()) + { + args.add(makeSetRuleButton(rule, option, true)); + args.add("w "); + } + if (!rule.suggestions().contains(RuleHelper.toRuleString(rule.value()))) + { + args.add(makeSetRuleButton(rule, RuleHelper.toRuleString(rule.value()), true)); + args.add("w "); + } + args.remove(args.size()-1); + return Messenger.c(args.toArray(new Object[0])); + } + + private Component makeSetRuleButton(CarpetRule rule, String option, boolean brackets) + { + String style = RuleHelper.isInDefaultValue(rule)?"g":(option.equalsIgnoreCase(RuleHelper.toRuleString(rule.defaultValue()))?"e":"y"); + if (option.equalsIgnoreCase(RuleHelper.toRuleString(rule.value()))) + { + style = style + "u"; + if (option.equalsIgnoreCase(RuleHelper.toRuleString(rule.defaultValue()))) + style = style + "b"; + } + String component = style + (brackets ? " [" : " ") + option + (brackets ? "]" : ""); + if (option.equalsIgnoreCase(RuleHelper.toRuleString(rule.value()))) + return Messenger.c(component); + return Messenger.c(component, "^g "+ tr(TranslationKeys.SWITCH_TO).formatted(option + (option.equals(RuleHelper.toRuleString(rule.defaultValue()))?" (default)":"")), "?/"+identifier+" " + rule.name() + " " + option); + } + + private int listSettings(CommandSourceStack source, String title, Collection> settings_list) + { + + Messenger.m(source,String.format("wb %s:",title)); + settings_list.forEach(e -> Messenger.m(source, displayInteractiveSetting(e))); + return settings_list.size(); + } + private int listAllSettings(CommandSourceStack source) + { + int count = listSettings(source, String.format(tr(TranslationKeys.CURRENT_SETTINGS_HEADER), fancyName), getNonDefault()); + + if (version != null) + Messenger.m(source, "g "+fancyName+" "+ tr(TranslationKeys.VERSION) + ": " + version); + + List tags = new ArrayList<>(); + tags.add("w " + tr(TranslationKeys.BROWSE_CATEGORIES) + ":\n"); + for (String t : getCategories()) + { + String translated = RuleHelper.translatedCategory(identifier(), t); + String translatedPlus = !translated.equals(t) ? "%s (%s)".formatted(translated, t) : t; + tags.add("c [" + translated +"]"); + tags.add("^g " + String.format(tr(TranslationKeys.LIST_ALL_CATEGORY), translatedPlus)); + tags.add("!/"+identifier+" list " + t); + tags.add("w "); + } + tags.remove(tags.size() - 1); + Messenger.m(source, tags.toArray(new Object[0])); + + return count; + } +} diff --git a/src/main/java/carpet/api/settings/Validator.java b/src/main/java/carpet/api/settings/Validator.java new file mode 100644 index 0000000..1ddc78c --- /dev/null +++ b/src/main/java/carpet/api/settings/Validator.java @@ -0,0 +1,61 @@ +package carpet.api.settings; + +import org.jetbrains.annotations.Nullable; + +import carpet.utils.Messenger; +import net.minecraft.commands.CommandSourceStack; + +/** + *

A {@link Validator} is a class that is able to validate the values given to a {@link CarpetRule}, cancelling rule + * modification if the value is not valid or even changing the value to a different one if needed.

+ * + *

Validators are used in the default implementation, {@link ParsedRule}, as the way of validating most input (other than validating + * it can actually be introduced in the rule), but can (and may) also be used in other {@link CarpetRule} implementations, though those are not + * required to.

+ * + * @see #validate(CommandSourceStack, CarpetRule, Object, String) + * + * @param The type of the rule's value + */ +public abstract class Validator +{ + /** + *

Validates whether the value passed to the given {@link CarpetRule} is valid as a new value for it.

+ * + *

Validators can also change the value that the rule is going to be set to by returning a value different to the + * one that has been passed to them

+ * + *

This method must not throw any exceptions.

+ * + * @param source The {@link CommandSourceStack} that originated this change, and should be further notified + * about it. May be {@code null} during rule synchronization. + * @param changingRule The {@link CarpetRule} that is being changed + * @param newValue The new value that is being set to the rule + * @param userInput The value that is being given to this rule by the user as a {@link String}, or a best-effort representation + * of the value as a {@link String}. This value may not correspond to the result of {@link RuleHelper#toRuleString(Object)} + * if other validators have modified the value, it's just a representation of the user's input. + * @return The new value to set the rule to instead, can return the {@code newValue} if the given value is correct. + * Returns {@code null} if the given value is not correct. + */ + public abstract T validate(@Nullable CommandSourceStack source, CarpetRule changingRule, T newValue, String userInput); + + /** + * @return A description of this {@link Validator}. It is used in the default {@link #notifyFailure(CommandSourceStack, CarpetRule, String)} + * implementation and to add extra information in {@link SettingsManager#printAllRulesToLog(String)} + */ + public String description() {return null;} + /** + *

Called after failing validation of the {@link CarpetRule} in order to notify the causing {@link CommandSourceStack} about the + * failure.

+ * + * @param source The {@link CommandSourceStack} that originated this change. It will never be {@code null} + * @param currentRule The {@link CarpetRule} that failed verification + * @param providedValue The {@link String} that was provided to the changing rule + */ + public void notifyFailure(CommandSourceStack source, CarpetRule currentRule, String providedValue) + { + Messenger.m(source, "r Wrong value for " + currentRule.name() + ": " + providedValue); + if (description() != null) + Messenger.m(source, "r " + description()); + } +} diff --git a/src/main/java/carpet/api/settings/Validators.java b/src/main/java/carpet/api/settings/Validators.java new file mode 100644 index 0000000..67cf099 --- /dev/null +++ b/src/main/java/carpet/api/settings/Validators.java @@ -0,0 +1,67 @@ +package carpet.api.settings; + +import java.util.List; + +import carpet.utils.CommandHelper; +import net.minecraft.commands.CommandSourceStack; + +/** + *

A collection of standard {@link Validator validators} you can use in your rules.

+ * + * @see Rule + * @see Rule#validators() + * + */ +public final class Validators { + private Validators() {}; + + /** + *

A {@link Validator} that checks whether the given {@link String} value was a valid command level as Carpet allows them, + * so either a number from 0 to 4, or one of the keywords {@code true}, {@code false} or {@code ops}

+ * + *

While there is no public API method for checking whether a source can execute a command, + * {@link CommandHelper#canUseCommand(CommandSourceStack, Object)} is not expected to change anytime soon.

+ * + */ + public static class CommandLevel extends Validator { + @Deprecated(forRemoval = true) // internal use only, will be made pckg private when phasing out old api + public static final List OPTIONS = List.of("true", "false", "ops", "0", "1", "2", "3", "4"); + @Override + public String validate(CommandSourceStack source, CarpetRule currentRule, String newValue, String userString) { + if (!OPTIONS.contains(newValue)) + { + return null; + } + return newValue; + } + @Override public String description() { return "Can be limited to 'ops' only, true/false for everyone/no one, or a custom permission level";} + } + + /** + *

A {@link Validator} that checks whether the entered number is equal or greater than {@code 0}.

+ */ + public static class NonNegativeNumber extends Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) + { + return newValue.doubleValue() >= 0 ? newValue : null; + } + @Override + public String description() { return "Must be a positive number or 0";} + } + + /** + *

A {@link Validator} that checks whether the entered number is between 0 and 1, inclusive.

+ */ + public static class Probablity extends Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule 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/commands/CounterCommand.java b/src/main/java/carpet/commands/CounterCommand.java new file mode 100644 index 0000000..2868381 --- /dev/null +++ b/src/main/java/carpet/commands/CounterCommand.java @@ -0,0 +1,95 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.helpers.HopperCounter; +import carpet.utils.Messenger; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.DyeColor; + +import static net.minecraft.commands.Commands.literal; + +/** + * Class for the /counter command which allows to use hoppers pointing into wool + */ +public class CounterCommand +{ + /** + * The method used to register the command and make it available for the players to use. + */ + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder commandBuilder = literal("counter") + .requires(c -> CarpetSettings.hopperCounters) + .executes(c -> listAllCounters(c.getSource(), false)) + .then(literal("reset") + .executes(c -> resetCounters(c.getSource()))); + + for (DyeColor dyeColor : DyeColor.values()) + { + commandBuilder.then( + literal(dyeColor.toString()) + .executes(c -> displayCounter(c.getSource(), dyeColor, false)) + .then(literal("reset") + .executes(c -> resetCounter(c.getSource(), dyeColor))) + .then(literal("realtime") + .executes(c -> displayCounter(c.getSource(), dyeColor, true))) + ); + } + dispatcher.register(commandBuilder); + } + + /** + * A method to prettily display the contents of a counter to the player + * @param color The counter colour whose contents we are querying. + * @param realtime Whether or not to display it as in-game time or IRL time, which accounts for less than 20TPS which + * would make it slower than IRL + */ + + private static int displayCounter(CommandSourceStack source, DyeColor color, boolean realtime) + { + HopperCounter counter = HopperCounter.getCounter(color); + + for (Component message: counter.format(source.getServer(), realtime, false)) + { + source.sendSuccess(() -> message, false); + } + return 1; + } + + private static int resetCounters(CommandSourceStack source) + { + HopperCounter.resetAll(source.getServer(), false); + Messenger.m(source, "w Restarted all counters"); + return 1; + } + + /** + * A method to reset the counter's timer to 0 and empty its items + * + * @param color The counter whose contents we want to reset + */ + private static int resetCounter(CommandSourceStack source, DyeColor color) + { + HopperCounter.getCounter(color).reset(source.getServer()); + Messenger.m(source, "w Restarted " + color + " counter"); + return 1; + } + + /** + * A method to prettily display all the counters to the player + * @param realtime Whether or not to display it as in-game time or IRL time, which accounts for less than 20TPS which + * would make it slower than IRL + */ + private static int listAllCounters(CommandSourceStack source, boolean realtime) + { + for (Component message: HopperCounter.formatAll(source.getServer(), realtime)) + { + source.sendSuccess(() -> message, false); + } + return 1; + } +} diff --git a/src/main/java/carpet/commands/DistanceCommand.java b/src/main/java/carpet/commands/DistanceCommand.java new file mode 100644 index 0000000..fe5c2e1 --- /dev/null +++ b/src/main/java/carpet/commands/DistanceCommand.java @@ -0,0 +1,44 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.utils.CommandHelper; +import carpet.utils.DistanceCalculator; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class DistanceCommand +{ + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder command = literal("distance"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandDistance)). + then(literal("from"). + executes( (c) -> DistanceCalculator.setStart(c.getSource(), c.getSource().getPosition())). + then(argument("from", Vec3Argument.vec3()). + executes( (c) -> DistanceCalculator.setStart( + c.getSource(), + Vec3Argument.getVec3(c, "from"))). + then(literal("to"). + executes((c) -> DistanceCalculator.distance( + c.getSource(), + Vec3Argument.getVec3(c, "from"), + c.getSource().getPosition())). + then(argument("to", Vec3Argument.vec3()). + executes( (c) -> DistanceCalculator.distance( + c.getSource(), + Vec3Argument.getVec3(c, "from"), + Vec3Argument.getVec3(c, "to") + )))))). + then(literal("to"). + executes( (c) -> DistanceCalculator.setEnd(c.getSource(), c.getSource().getPosition()) ). + then(argument("to", Vec3Argument.vec3()). + executes( (c) -> DistanceCalculator.setEnd(c.getSource(), Vec3Argument.getVec3(c, "to"))))); + dispatcher.register(command); + } +} diff --git a/src/main/java/carpet/commands/DrawCommand.java b/src/main/java/carpet/commands/DrawCommand.java new file mode 100644 index 0000000..df2e544 --- /dev/null +++ b/src/main/java/carpet/commands/DrawCommand.java @@ -0,0 +1,451 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import com.google.common.collect.Lists; +import com.mojang.brigadier.Command; +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 java.util.List; +import java.util.function.Predicate; + +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.blocks.BlockInput; +import net.minecraft.commands.arguments.blocks.BlockPredicateArgument; +import net.minecraft.commands.arguments.blocks.BlockStateArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import java.lang.Math; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; + +public class DrawCommand +{ + public static void register(CommandDispatcher dispatcher, final CommandBuildContext context) + { + LiteralArgumentBuilder command = literal("draw"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandDraw)). + then(literal("sphere"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(drawShape(c -> DrawCommand.drawSphere(c, false), context))))). + then(literal("ball"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(drawShape(c -> DrawCommand.drawSphere(c, true), context))))). + then(literal("diamond"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(drawShape(c -> DrawCommand.drawDiamond(c, true), context))))). + then(literal("pyramid"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(argument("height",IntegerArgumentType.integer(1)). + then(argument("pointing",StringArgumentType.word()).suggests( (c, b) -> suggest(new String[]{"up","down"},b)). + then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggest(new String[]{"y","x","z"},b)). + then(drawShape(c -> DrawCommand.drawPyramid(c, "square", true), context)))))))). + then(literal("cone"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(argument("height",IntegerArgumentType.integer(1)). + then(argument("pointing",StringArgumentType.word()).suggests( (c, b) -> suggest(new String[]{"up","down"},b)). + then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggest(new String[]{"y","x","z"},b)) + .then(drawShape(c -> DrawCommand.drawPyramid(c, "circle", true), context)))))))). + then(literal("cylinder"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(argument("height",IntegerArgumentType.integer(1)). + then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggest(new String[]{"y","x","z"},b)) + .then(drawShape(c -> DrawCommand.drawPrism(c, "circle"), context))))))). + then(literal("cuboid"). + then(argument("center", BlockPosArgument.blockPos()). + then(argument("radius", IntegerArgumentType.integer(1)). + then(argument("height",IntegerArgumentType.integer(1)). + then(argument("orientation",StringArgumentType.word()).suggests( (c, b) -> suggest(new String[]{"y","x","z"},b)) + .then(drawShape(c -> DrawCommand.drawPrism(c, "square"), context))))))); + dispatcher.register(command); + } + + @FunctionalInterface + private interface ArgumentExtractor + { + T apply(final CommandContext ctx, final String argName) throws CommandSyntaxException; + } + + private static RequiredArgumentBuilder + drawShape(Command drawer, CommandBuildContext commandBuildContext) + { + return argument("block", BlockStateArgument.block(commandBuildContext)). + executes(drawer) + .then(literal("replace") + .then(argument("filter", BlockPredicateArgument.blockPredicate(commandBuildContext)) + .executes(drawer))); + } + + 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( + ServerLevel world, BlockPos.MutableBlockPos mbpos, int x, int y, int z, + BlockInput block, Predicate replacement, + List list + ) + { + mbpos.set(x, y, z); + int success=0; + if (replacement == null || replacement.test(new BlockInWorld(world, mbpos, true))) + { + BlockEntity tileentity = world.getBlockEntity(mbpos); + if (tileentity instanceof Container) + { + ((Container) tileentity).clearContent(); + } + if (block.place(world, mbpos, 2)) + { + list.add(mbpos.immutable()); + ++success; + } + } + + return success; + } + + private static int drawSphere(CommandContext ctx, boolean solid) throws CommandSyntaxException + { + BlockPos pos; + int radius; + BlockInput block; + Predicate replacement; + try + { + pos = getArg(ctx, BlockPosArgument::getSpawnablePos, "center"); + radius = getArg(ctx, IntegerArgumentType::getInteger, "radius"); + block = getArg(ctx, BlockStateArgument::getBlock, "block"); + replacement = getArg(ctx, BlockPredicateArgument::getBlockPredicate, "filter", true); + } + catch (ErrorHandled ignored) { return 0; } + + int affected = 0; + ServerLevel world = ctx.getSource().getLevel(); + + 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; + final double invRadiusZ = 1 / radiusZ; + + final int ceilRadiusX = (int) Math.ceil(radiusX); + final int ceilRadiusY = (int) Math.ceil(radiusY); + final int ceilRadiusZ = (int) Math.ceil(radiusZ); + + BlockPos.MutableBlockPos mbpos = pos.mutable(); + List list = Lists.newArrayList(); + + double nextXn = 0; + + 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) + { + final double yn = nextYn; + nextYn = (y + 1) * invRadiusY; + double nextZn = 0; + 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) + { + break forX; + } + break forY; + } + break forZ; + } + + if (!solid && lengthSq(nextXn, yn, zn) <= 1 && lengthSq(xn, nextYn, zn) <= 1 && lengthSq(xn, yn, nextZn) <= 1) + { + continue; + } + + CarpetSettings.impendingFillSkipUpdates.set(!CarpetSettings.fillUpdates); + for (int xmod = -1; xmod < 2; xmod += 2) + { + for (int ymod = -1; ymod < 2; ymod += 2) + { + for (int zmod = -1; zmod < 2; zmod += 2) + { + affected+= setBlock(world, mbpos, + pos.getX() + xmod * x, pos.getY() + ymod * y, pos.getZ() + zmod * z, + block, replacement, list + ); + } + } + } + CarpetSettings.impendingFillSkipUpdates.set(false); + } + } + } + if (CarpetSettings.fillUpdates) + { + list.forEach(blockpos1 -> world.blockUpdated(blockpos1, world.getBlockState(blockpos1).getBlock())); + } + Messenger.m(ctx.getSource(), "gi Filled " + affected + " blocks"); + return affected; + } + + private static int drawDiamond(CommandContext ctx, boolean solid) throws CommandSyntaxException + { + BlockPos pos; + int radius; + BlockInput block; + Predicate replacement; + try + { + pos = getArg(ctx, BlockPosArgument::getSpawnablePos, "center"); + radius = getArg(ctx, IntegerArgumentType::getInteger, "radius"); + block = getArg(ctx, BlockStateArgument::getBlock, "block"); + replacement = getArg(ctx, BlockPredicateArgument::getBlockPredicate, "filter", true); + } + catch (ErrorHandled ignored) { return 0; } + + CommandSourceStack source = ctx.getSource(); + + int affected=0; + + BlockPos.MutableBlockPos mbpos = pos.mutable(); + List list = Lists.newArrayList(); + + ServerLevel world = source.getLevel(); + + CarpetSettings.impendingFillSkipUpdates.set(!CarpetSettings.fillUpdates); + + for (int r = 0; r < radius; ++r) + { + int y=r-radius+1; + for (int x = -r; x <= r; ++x) + { + 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); + } + } + + CarpetSettings.impendingFillSkipUpdates.set(false); + + if (CarpetSettings.fillUpdates) + { + list.forEach(p -> world.blockUpdated(p, world.getBlockState(p).getBlock())); + } + + Messenger.m(source, "gi Filled " + affected + " blocks"); + + return affected; + } + + private static int fillFlat( + ServerLevel world, BlockPos pos, int offset, double dr, boolean rectangle, String orientation, + BlockInput block, Predicate replacement, + List list, BlockPos.MutableBlockPos mbpos + ) + { + int successes=0; + int r = Mth.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; + BlockInput block; + Predicate replacement; + try + { + pos = getArg(ctx, BlockPosArgument::getSpawnablePos, "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, BlockStateArgument::getBlock, "block"); + replacement = getArg(ctx, BlockPredicateArgument::getBlockPredicate, "filter", true); + } + catch (ErrorHandled ignored) { return 0; } + + CommandSourceStack source = ctx.getSource(); + + int affected = 0; + BlockPos.MutableBlockPos mbpos = pos.mutable(); + + List list = Lists.newArrayList(); + + ServerLevel world = source.getLevel(); + + CarpetSettings.impendingFillSkipUpdates.set(!CarpetSettings.fillUpdates); + + boolean isSquare = base.equalsIgnoreCase("square"); + + for(int i =0; i ctx, String base){ + BlockPos pos; + double radius; + int height; + String orientation; + BlockInput block; + Predicate replacement; + try + { + pos = getArg(ctx, BlockPosArgument::getSpawnablePos, "center"); + radius = getArg(ctx, IntegerArgumentType::getInteger, "radius")+0.5D; + height = getArg(ctx, IntegerArgumentType::getInteger, "height"); + orientation = getArg(ctx, StringArgumentType::getString,"orientation"); + block = getArg(ctx, BlockStateArgument::getBlock, "block"); + replacement = getArg(ctx, BlockPredicateArgument::getBlockPredicate, "filter", true); + } + catch (ErrorHandled | CommandSyntaxException ignored) { return 0; } + + CommandSourceStack source = ctx.getSource(); + + int affected = 0; + BlockPos.MutableBlockPos mbpos = pos.mutable(); + + List list = Lists.newArrayList(); + + ServerLevel world = source.getLevel(); + + CarpetSettings.impendingFillSkipUpdates.set(!CarpetSettings.fillUpdates); + + boolean isSquare = base.equalsIgnoreCase("square"); + + for(int i =0; i dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder command = literal("info"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandInfo)). + then(literal("block"). + then(argument("block position", BlockPosArgument.blockPos()). + executes( (c) -> infoBlock( + c.getSource(), + BlockPosArgument.getSpawnablePos(c, "block position"), null)). + then(literal("grep"). + then(argument("regexp",greedyString()). + executes( (c) -> infoBlock( + c.getSource(), + BlockPosArgument.getSpawnablePos(c, "block position"), + getString(c, "regexp"))))))); + + dispatcher.register(command); + } + + public static void printBlock(List messages, CommandSourceStack source, String grep) + { + Messenger.m(source, ""); + if (grep != null) + { + Pattern p = Pattern.compile(grep); + Messenger.m(source, messages.get(0)); + for (int i = 1; i dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder literalargumentbuilder = Commands.literal("log"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandLog)). + executes((context) -> listLogs(context.getSource())). + then(Commands.literal("clear"). + executes( (c) -> unsubFromAll(c.getSource(), c.getSource().getTextName())). + then(Commands.argument("player", StringArgumentType.word()). + suggests( (c, b)-> suggest(c.getSource().getOnlinePlayerNames(),b)). + executes( (c) -> unsubFromAll(c.getSource(), getString(c, "player"))))); + + literalargumentbuilder.then(Commands.argument("log name",StringArgumentType.word()). + suggests( (c, b)-> suggest(LoggerRegistry.getLoggerNames(),b)). + executes( (c)-> toggleSubscription(c.getSource(), c.getSource().getTextName(), getString(c, "log name"))). + then(Commands.literal("clear"). + executes( (c) -> unsubFromLogger( + c.getSource(), + c.getSource().getTextName(), + getString(c, "log name")))). + then(Commands.argument("option", StringArgumentType.string()). + suggests( (c, b) -> suggest( + (LoggerRegistry.getLogger(getString(c, "log name"))==null + ?new String[]{} + :LoggerRegistry.getLogger(getString(c, "log name")).getOptions()), + b)). + executes( (c) -> subscribePlayer( + c.getSource(), + c.getSource().getTextName(), + getString(c, "log name"), + getString(c, "option"))). + then(Commands.argument("player", StringArgumentType.word()). + suggests( (c, b) -> suggest(c.getSource().getOnlinePlayerNames(),b)). + executes( (c) -> subscribePlayer( + c.getSource(), + getString(c, "player"), + getString(c, "log name"), + getString(c, "option")))))); + + dispatcher.register(literalargumentbuilder); + } + private static int listLogs(CommandSourceStack source) + { + Player player; + try + { + player = source.getPlayerOrException(); + } + catch (CommandSyntaxException e) + { + Messenger.m(source, "For players only"); + return 0; + } + Map subs = LoggerRegistry.getPlayerSubscriptions(source.getTextName()); + if (subs == null) + { + subs = new HashMap<>(); + } + List all_logs = new ArrayList<>(LoggerRegistry.getLoggerNames()); + Collections.sort(all_logs); + Messenger.m(player, "w _____________________"); + Messenger.m(player, "w Available logging options:"); + for (String lname: all_logs) + { + List comp = new ArrayList<>(); + String color = subs.containsKey(lname)?"w":"g"; + comp.add("w - "+lname+": "); + Logger logger = LoggerRegistry.getLogger(lname); + String [] options = logger.getOptions(); + if (options.length == 0) + { + if (subs.containsKey(lname)) + { + comp.add("l Subscribed "); + } + else + { + comp.add(color + " [Subscribe] "); + comp.add("^w subscribe to " + lname); + comp.add("!/log " + lname); + } + } + else + { + for (String option : logger.getOptions()) + { + if (subs.containsKey(lname) && subs.get(lname).equalsIgnoreCase(option)) + { + comp.add("l [" + option + "] "); + } else + { + comp.add(color + " [" + option + "] "); + comp.add("^w subscribe to " + lname + " " + option); + comp.add("!/log " + lname + " " + option); + } + + } + } + if (subs.containsKey(lname)) + { + comp.add("nb [X]"); + comp.add("^w Click to unsubscribe"); + comp.add("!/log "+lname); + } + Messenger.m(player,comp.toArray(new Object[0])); + } + return 1; + } + private static int unsubFromAll(CommandSourceStack source, String player_name) + { + Player player = source.getServer().getPlayerList().getPlayerByName(player_name); + if (player == null) + { + Messenger.m(source, "r No player specified"); + return 0; + } + for (String logname : LoggerRegistry.getLoggerNames()) + { + LoggerRegistry.unsubscribePlayer(player_name, logname); + } + Messenger.m(source, "gi Unsubscribed from all logs"); + return 1; + } + private static int unsubFromLogger(CommandSourceStack source, String player_name, String logname) + { + Player player = source.getServer().getPlayerList().getPlayerByName(player_name); + if (player == null) + { + Messenger.m(source, "r No player specified"); + return 0; + } + if (LoggerRegistry.getLogger(logname) == null) + { + Messenger.m(source, "r Unknown logger: ","rb "+logname); + return 0; + } + LoggerRegistry.unsubscribePlayer(player_name, logname); + Messenger.m(source, "gi Unsubscribed from "+logname); + return 1; + } + + private static int toggleSubscription(CommandSourceStack source, String player_name, String logName) + { + Player player = source.getServer().getPlayerList().getPlayerByName(player_name); + if (player == null) + { + Messenger.m(source, "r No player specified"); + return 0; + } + if (LoggerRegistry.getLogger(logName) == null) + { + Messenger.m(source, "r Unknown logger: ","rb "+logName); + return 0; + } + boolean subscribed = LoggerRegistry.togglePlayerSubscription(player_name, logName); + if (subscribed) + { + Messenger.m(source, "gi "+player_name+" subscribed to " + logName + "."); + } + else + { + Messenger.m(source, "gi "+player_name+" unsubscribed from " + logName + "."); + } + return 1; + } + private static int subscribePlayer(CommandSourceStack source, String player_name, String logname, String option) + { + Player player = source.getServer().getPlayerList().getPlayerByName(player_name); + if (player == null) + { + Messenger.m(source, "r No player specified"); + return 0; + } + if (LoggerRegistry.getLogger(logname) == null) + { + Messenger.m(source, "r Unknown logger: ","rb "+logname); + return 0; + } + if (!LoggerRegistry.getLogger(logname).isOptionValid(option)) + { + Messenger.m(source, "r Invalid option: ", "rb "+option); + return 0; + } + LoggerRegistry.subscribePlayer(player_name, logname, option); + if (option != null) + { + Messenger.m(source, "gi Subscribed to " + logname + "(" + option + ")"); + } + else + { + Messenger.m(source, "gi Subscribed to " + logname); + } + return 1; + } +} diff --git a/src/main/java/carpet/commands/MobAICommand.java b/src/main/java/carpet/commands/MobAICommand.java new file mode 100644 index 0000000..5345583 --- /dev/null +++ b/src/main/java/carpet/commands/MobAICommand.java @@ -0,0 +1,45 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.utils.CommandHelper; +import carpet.utils.MobAI; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.registries.Registries; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; +import static net.minecraft.commands.arguments.ResourceArgument.getSummonableEntityType; +import static net.minecraft.commands.arguments.ResourceArgument.resource; + +public class MobAICommand +{ + public static void register(CommandDispatcher dispatcher, final CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder command = literal("track"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandTrackAI)). + then(argument("entity type", resource(commandBuildContext, Registries.ENTITY_TYPE)). + + suggests( (c, b) -> suggest(MobAI.availbleTypes(c.getSource()), b)). + then(literal("clear").executes( (c) -> + { + MobAI.clearTracking(c.getSource().getServer(), getSummonableEntityType(c, "entity type").value()); + return 1; + } + )). + then(argument("aspect", StringArgumentType.word()). + suggests( (c, b) -> suggest(MobAI.availableFor(getSummonableEntityType(c, "entity type").value()),b)). + executes( (c) -> { + MobAI.startTracking( + getSummonableEntityType(c, "entity type").value(), + MobAI.TrackingType.valueOf(StringArgumentType.getString(c, "aspect").toUpperCase()) + ); + return 1; + }))); + dispatcher.register(command); + } +} diff --git a/src/main/java/carpet/commands/PerimeterInfoCommand.java b/src/main/java/carpet/commands/PerimeterInfoCommand.java new file mode 100644 index 0000000..bc47809 --- /dev/null +++ b/src/main/java/carpet/commands/PerimeterInfoCommand.java @@ -0,0 +1,82 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import carpet.utils.PerimeterDiagnostics; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.arguments.ResourceArgument.getSummonableEntityType; +import static net.minecraft.commands.arguments.ResourceArgument.resource; + +public class PerimeterInfoCommand +{ + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder command = literal("perimeterinfo"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandPerimeterInfo)). + executes( (c) -> perimeterDiagnose( + c.getSource(), + BlockPos.containing(c.getSource().getPosition()), + null)). + then(argument("center position", BlockPosArgument.blockPos()). + executes( (c) -> perimeterDiagnose( + c.getSource(), + BlockPosArgument.getSpawnablePos(c, "center position"), + null)). + then(argument("mob", resource(commandBuildContext, Registries.ENTITY_TYPE)). + suggests(SuggestionProviders.SUMMONABLE_ENTITIES). + executes( (c) -> perimeterDiagnose( + c.getSource(), + BlockPosArgument.getSpawnablePos(c, "center position"), + getSummonableEntityType(c, "mob").key().location().toString() + )))); + dispatcher.register(command); + } + + private static int perimeterDiagnose(CommandSourceStack source, BlockPos pos, String mobId) + { + CompoundTag nbttagcompound = new CompoundTag(); + Mob entityliving = null; + if (mobId != null) + { + nbttagcompound.putString("id", mobId); + Entity baseEntity = EntityType.loadEntityRecursive(nbttagcompound, source.getLevel(), (entity_1x) -> { + entity_1x.moveTo(new BlockPos(pos.getX(), source.getLevel().getMinBuildHeight()-10, pos.getZ()), entity_1x.getYRot(), entity_1x. getXRot()); + return !source.getLevel().addWithUUID(entity_1x) ? null : entity_1x; + }); + if (!(baseEntity instanceof Mob)) + { + Messenger.m(source, "r /perimeterinfo requires a mob entity to test against."); + if (baseEntity != null) baseEntity.discard(); + return 0; + } + entityliving = (Mob) baseEntity; + } + PerimeterDiagnostics.Result res = PerimeterDiagnostics.countSpots(source.getLevel(), pos, entityliving); + + Messenger.m(source, "w Spawning spaces around ",Messenger.tp("c",pos), "w :"); + Messenger.m(source, "w potential in-liquid: ","wb "+res.liquid); + Messenger.m(source, "w potential on-ground: ","wb "+res.ground); + if (entityliving != null) + { + Messenger.m(source, "w ", entityliving.getDisplayName() ,"w : ","wb "+res.specific); + res.samples.forEach(bp -> Messenger.m(source, "w ", Messenger.tp("c", bp))); + entityliving.discard(); // dicard // remove(); + } + return 1; + } +} diff --git a/src/main/java/carpet/commands/PlayerCommand.java b/src/main/java/carpet/commands/PlayerCommand.java new file mode 100644 index 0000000..65a0053 --- /dev/null +++ b/src/main/java/carpet/commands/PlayerCommand.java @@ -0,0 +1,343 @@ +package carpet.commands; + +import carpet.helpers.EntityPlayerActionPack; +import carpet.helpers.EntityPlayerActionPack.Action; +import carpet.helpers.EntityPlayerActionPack.ActionType; +import carpet.CarpetSettings; +import carpet.fakes.ServerPlayerInterface; +import carpet.patches.EntityPlayerMPFake; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.SharedConstants; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.GameModeArgument; +import net.minecraft.commands.arguments.coordinates.RotationArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.UUIDUtil; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; + +public class PlayerCommand +{ + // TODO: allow any order like execute + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder command = literal("player") + .requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandPlayer)) + .then(argument("player", StringArgumentType.word()) + .suggests((c, b) -> suggest(getPlayerSuggestions(c.getSource()), b)) + .then(literal("stop").executes(manipulation(EntityPlayerActionPack::stopAll))) + .then(literal("stopm").executes(manipulation(EntityPlayerActionPack::stopMovement))) + .then(makeActionCommand("use", ActionType.USE)) + .then(makeActionCommand("jump", ActionType.JUMP)) + .then(makeActionCommand("attack", ActionType.ATTACK)) + .then(makeActionCommand("drop", ActionType.DROP_ITEM)) + .then(makeDropCommand("drop", false)) + .then(makeActionCommand("dropStack", ActionType.DROP_STACK)) + .then(makeDropCommand("dropStack", true)) + .then(makeActionCommand("swapHands", ActionType.SWAP_HANDS)) + .then(literal("hotbar") + .then(argument("slot", IntegerArgumentType.integer(1, 9)) + .executes(c -> manipulate(c, ap -> ap.setSlot(IntegerArgumentType.getInteger(c, "slot")))))) + .then(literal("kill").executes(PlayerCommand::kill)) + .then(literal("shadow"). executes(PlayerCommand::shadow)) + .then(literal("mount").executes(manipulation(ap -> ap.mount(true))) + .then(literal("anything").executes(manipulation(ap -> ap.mount(false))))) + .then(literal("dismount").executes(manipulation(EntityPlayerActionPack::dismount))) + .then(literal("sneak").executes(manipulation(ap -> ap.setSneaking(true)))) + .then(literal("unsneak").executes(manipulation(ap -> ap.setSneaking(false)))) + .then(literal("sprint").executes(manipulation(ap -> ap.setSprinting(true)))) + .then(literal("unsprint").executes(manipulation(ap -> ap.setSprinting(false)))) + .then(literal("look") + .then(literal("north").executes(manipulation(ap -> ap.look(Direction.NORTH)))) + .then(literal("south").executes(manipulation(ap -> ap.look(Direction.SOUTH)))) + .then(literal("east").executes(manipulation(ap -> ap.look(Direction.EAST)))) + .then(literal("west").executes(manipulation(ap -> ap.look(Direction.WEST)))) + .then(literal("up").executes(manipulation(ap -> ap.look(Direction.UP)))) + .then(literal("down").executes(manipulation(ap -> ap.look(Direction.DOWN)))) + .then(literal("at").then(argument("position", Vec3Argument.vec3()) + .executes(c -> manipulate(c, ap -> ap.lookAt(Vec3Argument.getVec3(c, "position")))))) + .then(argument("direction", RotationArgument.rotation()) + .executes(c -> manipulate(c, ap -> ap.look(RotationArgument.getRotation(c, "direction").getRotation(c.getSource()))))) + ).then(literal("turn") + .then(literal("left").executes(manipulation(ap -> ap.turn(-90, 0)))) + .then(literal("right").executes(manipulation(ap -> ap.turn(90, 0)))) + .then(literal("back").executes(manipulation(ap -> ap.turn(180, 0)))) + .then(argument("rotation", RotationArgument.rotation()) + .executes(c -> manipulate(c, ap -> ap.turn(RotationArgument.getRotation(c, "rotation").getRotation(c.getSource()))))) + ).then(literal("move").executes(manipulation(EntityPlayerActionPack::stopMovement)) + .then(literal("forward").executes(manipulation(ap -> ap.setForward(1)))) + .then(literal("backward").executes(manipulation(ap -> ap.setForward(-1)))) + .then(literal("left").executes(manipulation(ap -> ap.setStrafing(1)))) + .then(literal("right").executes(manipulation(ap -> ap.setStrafing(-1)))) + ).then(literal("spawn").executes(PlayerCommand::spawn) + .then(literal("in").requires((player) -> player.hasPermission(2)) + .then(argument("gamemode", GameModeArgument.gameMode()) + .executes(PlayerCommand::spawn))) + .then(literal("at").then(argument("position", Vec3Argument.vec3()).executes(PlayerCommand::spawn) + .then(literal("facing").then(argument("direction", RotationArgument.rotation()).executes(PlayerCommand::spawn) + .then(literal("in").then(argument("dimension", DimensionArgument.dimension()).executes(PlayerCommand::spawn) + .then(literal("in").requires((player) -> player.hasPermission(2)) + .then(argument("gamemode", GameModeArgument.gameMode()) + .executes(PlayerCommand::spawn) + ))) + ))) + )) + ) + ); + dispatcher.register(command); + } + + private static LiteralArgumentBuilder makeActionCommand(String actionName, ActionType type) + { + return literal(actionName) + .executes(manipulation(ap -> ap.start(type, Action.once()))) + .then(literal("once").executes(manipulation(ap -> ap.start(type, Action.once())))) + .then(literal("continuous").executes(manipulation(ap -> ap.start(type, Action.continuous())))) + .then(literal("interval").then(argument("ticks", IntegerArgumentType.integer(1)) + .executes(c -> manipulate(c, ap -> ap.start(type, Action.interval(IntegerArgumentType.getInteger(c, "ticks"))))))); + } + + private static LiteralArgumentBuilder makeDropCommand(String actionName, boolean dropAll) + { + return literal(actionName) + .then(literal("all").executes(manipulation(ap -> ap.drop(-2, dropAll)))) + .then(literal("mainhand").executes(manipulation(ap -> ap.drop(-1, dropAll)))) + .then(literal("offhand").executes(manipulation(ap -> ap.drop(40, dropAll)))) + .then(argument("slot", IntegerArgumentType.integer(0, 40)). + executes(c -> manipulate(c, ap -> ap.drop(IntegerArgumentType.getInteger(c, "slot"), dropAll)))); + } + + private static Collection getPlayerSuggestions(CommandSourceStack source) + { + Set players = new LinkedHashSet<>(List.of("Steve", "Alex")); + players.addAll(source.getOnlinePlayerNames()); + return players; + } + + private static ServerPlayer getPlayer(CommandContext context) + { + String playerName = StringArgumentType.getString(context, "player"); + MinecraftServer server = context.getSource().getServer(); + return server.getPlayerList().getPlayerByName(playerName); + } + + private static boolean cantManipulate(CommandContext context) + { + Player player = getPlayer(context); + CommandSourceStack source = context.getSource(); + if (player == null) + { + Messenger.m(source, "r Can only manipulate existing players"); + return true; + } + Player sender = source.getPlayer(); + if (sender == null) + { + return false; + } + + if (!source.getServer().getPlayerList().isOp(sender.getGameProfile())) + { + if (sender != player && !(player instanceof EntityPlayerMPFake)) + { + Messenger.m(source, "r Non OP players can't control other real players"); + return true; + } + } + return false; + } + + private static boolean cantReMove(CommandContext context) + { + if (cantManipulate(context)) return true; + Player player = getPlayer(context); + if (player instanceof EntityPlayerMPFake) return false; + Messenger.m(context.getSource(), "r Only fake players can be moved or killed"); + return true; + } + + private static boolean cantSpawn(CommandContext context) + { + String playerName = StringArgumentType.getString(context, "player"); + MinecraftServer server = context.getSource().getServer(); + PlayerList manager = server.getPlayerList(); + + if (manager.getPlayerByName(playerName) != null) + { + Messenger.m(context.getSource(), "r Player ", "rb " + playerName, "r is already logged on"); + return true; + } + GameProfile profile = server.getProfileCache().get(playerName).orElse(null); + if (profile == null) + { + if (!CarpetSettings.allowSpawningOfflinePlayers) + { + Messenger.m(context.getSource(), "r Player "+playerName+" is either banned by Mojang, or auth servers are down. " + + "Banned players can only be summoned in Singleplayer and in servers in off-line mode."); + return true; + } else { + profile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(playerName), playerName); + } + } + if (manager.getBans().isBanned(profile)) + { + Messenger.m(context.getSource(), "r Player ", "rb " + playerName, "r is banned on this server"); + return true; + } + if (manager.isUsingWhitelist() && manager.isWhiteListed(profile) && !context.getSource().hasPermission(2)) + { + Messenger.m(context.getSource(), "r Whitelisted players can only be spawned by operators"); + return true; + } + return false; + } + + private static int kill(CommandContext context) + { + if (cantReMove(context)) return 0; + getPlayer(context).kill(); + return 1; + } + + @FunctionalInterface + interface SupplierWithCSE + { + T get() throws CommandSyntaxException; + } + + private static T getArgOrDefault(SupplierWithCSE getter, T defaultValue) throws CommandSyntaxException + { + try + { + return getter.get(); + } + catch (IllegalArgumentException e) + { + return defaultValue; + } + } + + private static int spawn(CommandContext context) throws CommandSyntaxException + { + if (cantSpawn(context)) return 0; + + CommandSourceStack source = context.getSource(); + Vec3 pos = getArgOrDefault( + () -> Vec3Argument.getVec3(context, "position"), + source.getPosition() + ); + Vec2 facing = getArgOrDefault( + () -> RotationArgument.getRotation(context, "direction").getRotation(source), + source.getRotation() + ); + ResourceKey dimType = getArgOrDefault( + () -> DimensionArgument.getDimension(context, "dimension").dimension(), + source.getLevel().dimension() + ); + GameType mode = GameType.CREATIVE; + boolean flying = false; + if (source.getEntity() instanceof ServerPlayer sender) + { + mode = sender.gameMode.getGameModeForPlayer(); + flying = sender.getAbilities().flying; + } + try { + mode = GameModeArgument.getGameMode(context, "gamemode"); + } catch (IllegalArgumentException notPresent) {} + + if (mode == GameType.SPECTATOR) + { + // Force override flying to true for spectator players, or they will fell out of the world. + flying = true; + } else if (mode.isSurvival()) + { + // Force override flying to false for survival-like players, or they will fly too + flying = false; + } + String playerName = StringArgumentType.getString(context, "player"); + if (playerName.length() > maxNameLength(source.getServer())) + { + Messenger.m(source, "rb Player name: " + playerName + " is too long"); + return 0; + } + + if (!Level.isInSpawnableBounds(BlockPos.containing(pos))) + { + Messenger.m(source, "rb Player " + playerName + " cannot be placed outside of the world"); + return 0; + } + boolean success = EntityPlayerMPFake.createFake(playerName, source.getServer(), pos, facing.y, facing.x, dimType, mode, flying); + if (!success) { + Messenger.m(source, "rb Player " + playerName + " doesn't exist and cannot spawn in online mode. " + + "Turn the server offline or the allowSpawningOfflinePlayers on to spawn non-existing players"); + return 0; + }; + return 1; + } + + private static int maxNameLength(MinecraftServer server) + { + return server.getPort() >= 0 ? SharedConstants.MAX_PLAYER_NAME_LENGTH : 40; + } + + private static int manipulate(CommandContext context, Consumer action) + { + if (cantManipulate(context)) return 0; + ServerPlayer player = getPlayer(context); + action.accept(((ServerPlayerInterface) player).getActionPack()); + return 1; + } + + private static Command manipulation(Consumer action) + { + return c -> manipulate(c, action); + } + + private static int shadow(CommandContext context) + { + if (cantManipulate(context)) return 0; + + ServerPlayer player = getPlayer(context); + if (player instanceof EntityPlayerMPFake) + { + Messenger.m(context.getSource(), "r Cannot shadow fake players"); + return 0; + } + if (player.getServer().isSingleplayerOwner(player.getGameProfile())) { + Messenger.m(context.getSource(), "r Cannot shadow single-player server owner"); + return 0; + } + + EntityPlayerMPFake.createShadow(player.server, player); + return 1; + } +} diff --git a/src/main/java/carpet/commands/ProfileCommand.java b/src/main/java/carpet/commands/ProfileCommand.java new file mode 100644 index 0000000..553f31c --- /dev/null +++ b/src/main/java/carpet/commands/ProfileCommand.java @@ -0,0 +1,46 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.utils.CarpetProfiler; +import carpet.utils.CommandHelper; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; +import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class ProfileCommand +{ + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder literalargumentbuilder = literal("profile"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandProfile)). + executes( (c) -> healthReport(c.getSource(), 100)). + then(literal("health"). + executes( (c) -> healthReport(c.getSource(), 100)). + then(argument("ticks", integer(20,24000)). + executes( (c) -> healthReport(c.getSource(), getInteger(c, "ticks"))))). + then(literal("entities"). + executes((c) -> healthEntities(c.getSource(), 100)). + then(argument("ticks", integer(20,24000)). + executes((c) -> healthEntities(c.getSource(), getInteger(c, "ticks"))))); + dispatcher.register(literalargumentbuilder); + } + + public static int healthReport(CommandSourceStack source, int ticks) + { + CarpetProfiler.prepare_tick_report(source, ticks); + return 1; + } + + public static int healthEntities(CommandSourceStack source, int ticks) + { + CarpetProfiler.prepare_entity_report(source, ticks); + return 1; + } +} diff --git a/src/main/java/carpet/commands/SpawnCommand.java b/src/main/java/carpet/commands/SpawnCommand.java new file mode 100644 index 0000000..e8824af --- /dev/null +++ b/src/main/java/carpet/commands/SpawnCommand.java @@ -0,0 +1,250 @@ +package carpet.commands; + +import carpet.CarpetSettings; +import carpet.fakes.SpawnGroupInterface; +import carpet.helpers.HopperCounter; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import carpet.utils.SpawnReporter; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.server.ServerTickRateManager; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.levelgen.structure.BoundingBox; + +import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger; +import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.string; +import static com.mojang.brigadier.arguments.StringArgumentType.word; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; + +public class SpawnCommand +{ + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder literalargumentbuilder = literal("spawn"). + requires((player) -> CommandHelper.canUseCommand(player, CarpetSettings.commandSpawn)); + + literalargumentbuilder. + then(literal("list"). + then(argument("pos", BlockPosArgument.blockPos()). + executes( (c) -> listSpawns(c.getSource(), BlockPosArgument.getSpawnablePos(c, "pos"))))). + then(literal("tracking"). + executes( (c) -> printTrackingReport(c.getSource())). + then(literal("start"). + executes( (c) -> startTracking(c.getSource(), null)). + then(argument("from", BlockPosArgument.blockPos()). + then(argument("to", BlockPosArgument.blockPos()). + executes( (c) -> startTracking( + c.getSource(), + BoundingBox.fromCorners( + BlockPosArgument.getSpawnablePos(c, "from"), + BlockPosArgument.getSpawnablePos(c, "to"))))))). + then(literal("stop"). + executes( (c) -> stopTracking(c.getSource()))). + then(argument("type", word()). + suggests( (c, b) -> suggest(Arrays.stream(SpawnReporter.cachedMobCategories()).map(MobCategory::getName),b)). + executes( (c) -> recentSpawnsForType(c.getSource(), getString(c, "type"))))). + then(literal("test"). + executes( (c)-> runTest(c.getSource(), 72000, null)). + then(argument("ticks", integer(10)). + executes( (c)-> runTest( + c.getSource(), + getInteger(c, "ticks"), + null)). + then(argument("counter", word()). + suggests( (c, b) -> suggest(Arrays.stream(DyeColor.values()).map(DyeColor::toString),b)). + executes((c)-> runTest( + c.getSource(), + getInteger(c, "ticks"), + getString(c, "counter")))))). + then(literal("mocking"). + then(argument("to do or not to do?", BoolArgumentType.bool()). + executes( (c) -> toggleMocking(c.getSource(), BoolArgumentType.getBool(c, "to do or not to do?"))))). + then(literal("rates"). + executes( (c) -> generalMobcaps(c.getSource())). + then(literal("reset"). + executes( (c) -> resetSpawnRates(c.getSource()))). + then(argument("type", word()). + suggests( (c, b) -> suggest(Arrays.stream(SpawnReporter.cachedMobCategories()).map(MobCategory::getName),b)). + then(argument("rounds", integer(0)). + suggests( (c, b) -> suggest(new String[]{"1"},b)). + executes( (c) -> setSpawnRates( + c.getSource(), + getString(c, "type"), + getInteger(c, "rounds")))))). + then(literal("mobcaps"). + executes( (c) -> generalMobcaps(c.getSource())). + then(literal("set"). + then(argument("cap (hostile)", integer(1,1400)). + executes( (c) -> setMobcaps(c.getSource(), getInteger(c, "cap (hostile)"))))). + then(argument("dimension", DimensionArgument.dimension()). + executes( (c)-> mobcapsForDimension(c.getSource(), DimensionArgument.getDimension(c, "dimension"))))). + then(literal("entities"). + executes( (c) -> generalMobcaps(c.getSource()) ). + then(argument("type", string()). + suggests( (c, b)->suggest(Arrays.stream(SpawnReporter.cachedMobCategories()).map(MobCategory::getName), b)). + executes( (c) -> listEntitiesOfType(c.getSource(), getString(c, "type"), false)). + then(literal("all").executes( (c) -> listEntitiesOfType(c.getSource(), getString(c, "type"), true))))); + + dispatcher.register(literalargumentbuilder); + } + + private static final Map MOB_CATEGORY_MAP = Arrays.stream(SpawnReporter.cachedMobCategories()).collect(Collectors.toMap(MobCategory::getName, Function.identity())); + + private static MobCategory getCategory(String string) throws CommandSyntaxException + { + if (!Arrays.stream(SpawnReporter.cachedMobCategories()).map(MobCategory::getName).collect(Collectors.toSet()).contains(string)) + { + throw new SimpleCommandExceptionType(Messenger.c("r Wrong mob type: "+string+" should be "+ Arrays.stream(SpawnReporter.cachedMobCategories()).map(MobCategory::getName).collect(Collectors.joining(", ")))).create(); + } + return MOB_CATEGORY_MAP.get(string.toLowerCase(Locale.ROOT)); + } + + + private static int listSpawns(CommandSourceStack source, BlockPos pos) + { + Messenger.send(source, SpawnReporter.report(pos, source.getLevel())); + return 1; + } + + private static int printTrackingReport(CommandSourceStack source) + { + Messenger.send(source, SpawnReporter.makeTrackingReport(source.getLevel())); + return 1; + } + + private static int startTracking(CommandSourceStack source, BoundingBox filter) + { + if (SpawnReporter.trackingSpawns()) + { + Messenger.m(source, "r You are already tracking spawning."); + return 0; + } + SpawnReporter.startTracking(source.getServer(), filter); + Messenger.m(source, "gi Spawning tracking started."); + return 1; + } + + private static int stopTracking(CommandSourceStack source) + { + Messenger.send(source, SpawnReporter.makeTrackingReport(source.getLevel())); + SpawnReporter.stopTracking(source.getServer()); + Messenger.m(source, "gi Spawning tracking stopped."); + return 1; + } + + private static int recentSpawnsForType(CommandSourceStack source, String mob_type) throws CommandSyntaxException + { + MobCategory cat = getCategory(mob_type); + Messenger.send(source, SpawnReporter.getRecentSpawns(source.getLevel(), cat)); + return 1; + } + + private static int runTest(CommandSourceStack source, int ticks, String counter) + { + // Start tracking + SpawnReporter.startTracking(source.getServer(), null); + // Reset counter + if (counter == null) + { + HopperCounter.resetAll(source.getServer(), false); + } + else + { + HopperCounter hCounter = HopperCounter.getCounter(counter); + if (hCounter != null) + hCounter.reset(source.getServer()); + } + + + // tick warp 0 + ServerTickRateManager trm = source.getServer().tickRateManager(); + // stop warp + // unnecessary + // start warp + trm.requestGameToSprint(ticks); + Messenger.m(source, String.format("gi Started spawn test for %d ticks", ticks)); + return 1; + } + + private static int toggleMocking(CommandSourceStack source, boolean domock) + { + if (domock) + { + SpawnReporter.initializeMocking(); + Messenger.m(source, "gi Mob spawns will now be mocked."); + } + else + { + SpawnReporter.stopMocking(); + Messenger.m(source, "gi Normal mob spawning."); + } + return 1; + } + + private static int generalMobcaps(CommandSourceStack source) + { + Messenger.send(source, SpawnReporter.printMobcapsForDimension(source.getLevel(), true)); + return 1; + } + + private static int resetSpawnRates(CommandSourceStack source) + { + for (MobCategory s: SpawnReporter.spawn_tries.keySet()) + { + SpawnReporter.spawn_tries.put(s,1); + } + Messenger.m(source, "gi Spawn rates brought to 1 round per tick for all groups."); + + return 1; + } + + private static int setSpawnRates(CommandSourceStack source, String mobtype, int rounds) throws CommandSyntaxException + { + MobCategory cat = getCategory(mobtype); + SpawnReporter.spawn_tries.put(cat, rounds); + Messenger.m(source, "gi "+mobtype+" mobs will now spawn "+rounds+" times per tick"); + return 1; + } + + private static int setMobcaps(CommandSourceStack source, int hostile_cap) + { + double desired_ratio = (double)hostile_cap/ ((SpawnGroupInterface)(Object)MobCategory.MONSTER).getInitialSpawnCap(); + SpawnReporter.mobcap_exponent = 4.0*Math.log(desired_ratio)/Math.log(2.0); + Messenger.m(source, String.format("gi Mobcaps for hostile mobs changed to %d, other groups will follow", hostile_cap)); + return 1; + } + + private static int mobcapsForDimension(CommandSourceStack source, ServerLevel world) + { + Messenger.send(source, SpawnReporter.printMobcapsForDimension(world, true)); + return 1; + } + + private static int listEntitiesOfType(CommandSourceStack source, String mobtype, boolean all) throws CommandSyntaxException + { + MobCategory cat = getCategory(mobtype); + Messenger.send(source, SpawnReporter.printEntitiesByType(cat, source.getLevel(), all)); + return 1; + } +} diff --git a/src/main/java/carpet/commands/TestCommand.java b/src/main/java/carpet/commands/TestCommand.java new file mode 100644 index 0000000..316006c --- /dev/null +++ b/src/main/java/carpet/commands/TestCommand.java @@ -0,0 +1,34 @@ +package carpet.commands; + +import carpet.CarpetServer; +import carpet.utils.Messenger; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; + +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static com.mojang.brigadier.arguments.StringArgumentType.word; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class TestCommand +{ + public static void register(CommandDispatcher dispatcher) + { + dispatcher.register(literal("testcarpet"). + then(literal("dump"). + executes((c) -> CarpetServer.settingsManager.dumpAllRulesToStream(System.out, null)). + then(argument("category", word()). + executes( (c) -> CarpetServer.settingsManager.dumpAllRulesToStream(System.out, getString(c, "category"))))). + then(argument("first",word()). + executes( (c)-> test(c, getString(c, "first")+" 1"))). + then(argument("second", word()). + executes( (c)-> test(c, getString(c, "second")+" 2")))); + } + + private static int test(CommandContext c, String term) + { + Messenger.m(c.getSource(),term.substring(0,1)+" "+term+": how did you get here?"); + return 1; + } +} diff --git a/src/main/java/carpet/fakes/AbstractContainerMenuInterface.java b/src/main/java/carpet/fakes/AbstractContainerMenuInterface.java new file mode 100644 index 0000000..536285d --- /dev/null +++ b/src/main/java/carpet/fakes/AbstractContainerMenuInterface.java @@ -0,0 +1,13 @@ +package carpet.fakes; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.item.crafting.RecipeHolder; + +public interface AbstractContainerMenuInterface +{ + DataSlot getDataSlot(int index); + boolean callButtonClickListener(int button, Player player); + boolean callSelectRecipeListener(ServerPlayer player, RecipeHolder recipe, boolean craftAll); +} diff --git a/src/main/java/carpet/fakes/BiomeInterface.java b/src/main/java/carpet/fakes/BiomeInterface.java new file mode 100644 index 0000000..3279150 --- /dev/null +++ b/src/main/java/carpet/fakes/BiomeInterface.java @@ -0,0 +1,7 @@ +package carpet.fakes; + +import net.minecraft.world.level.biome.Biome; + +public interface BiomeInterface { + Biome.ClimateSettings getClimateSettings(); +} diff --git a/src/main/java/carpet/fakes/BlockEntityInterface.java b/src/main/java/carpet/fakes/BlockEntityInterface.java new file mode 100644 index 0000000..7847ce6 --- /dev/null +++ b/src/main/java/carpet/fakes/BlockEntityInterface.java @@ -0,0 +1,8 @@ +package carpet.fakes; + +import net.minecraft.core.BlockPos; + +public interface BlockEntityInterface +{ + void setCMPos(BlockPos pos); +} diff --git a/src/main/java/carpet/fakes/BlockPistonBehaviourInterface.java b/src/main/java/carpet/fakes/BlockPistonBehaviourInterface.java new file mode 100644 index 0000000..b88bf63 --- /dev/null +++ b/src/main/java/carpet/fakes/BlockPistonBehaviourInterface.java @@ -0,0 +1,24 @@ +package carpet.fakes; + +import carpet.mixins.PistonStructureResolver_customStickyMixin; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.piston.PistonStructureResolver; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Opt-in Interface that allows for more control on a blocks interaction within the {@link PistonStructureResolver} via {@link PistonStructureResolver_customStickyMixin} + */ +public interface BlockPistonBehaviourInterface { + + /** + * @return whether this block is sticky in any way when moved by pistons + */ + boolean isSticky(BlockState state); + + /** + * @return whether the neighboring block is pulled along if this block is moved by pistons + */ + boolean isStickyToNeighbor(Level level, BlockPos pos, BlockState state, BlockPos neighborPos, BlockState neighborState, Direction dir, Direction moveDir); +} diff --git a/src/main/java/carpet/fakes/BlockPredicateInterface.java b/src/main/java/carpet/fakes/BlockPredicateInterface.java new file mode 100644 index 0000000..833b207 --- /dev/null +++ b/src/main/java/carpet/fakes/BlockPredicateInterface.java @@ -0,0 +1,16 @@ +package carpet.fakes; + +import carpet.script.value.Value; +import java.util.Map; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +public interface BlockPredicateInterface +{ + BlockState getCMBlockState(); + TagKey getCMBlockTagKey(); + Map getCMProperties(); + CompoundTag getCMDataTag(); +} diff --git a/src/main/java/carpet/fakes/BlockStateArgumentInterface.java b/src/main/java/carpet/fakes/BlockStateArgumentInterface.java new file mode 100644 index 0000000..20610ed --- /dev/null +++ b/src/main/java/carpet/fakes/BlockStateArgumentInterface.java @@ -0,0 +1,8 @@ +package carpet.fakes; + +import net.minecraft.nbt.CompoundTag; + +public interface BlockStateArgumentInterface +{ + CompoundTag getCMTag(); +} diff --git a/src/main/java/carpet/fakes/ChunkHolderInterface.java b/src/main/java/carpet/fakes/ChunkHolderInterface.java new file mode 100644 index 0000000..6c5464a --- /dev/null +++ b/src/main/java/carpet/fakes/ChunkHolderInterface.java @@ -0,0 +1,13 @@ +package carpet.fakes; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; + +public interface ChunkHolderInterface +{ + //CompletableFuture> setDefaultProtoChunk(ChunkPos chpos, BlockableEventLoop executor, ServerLevel world); +} diff --git a/src/main/java/carpet/fakes/ChunkTicketManagerInterface.java b/src/main/java/carpet/fakes/ChunkTicketManagerInterface.java new file mode 100644 index 0000000..2a01ed8 --- /dev/null +++ b/src/main/java/carpet/fakes/ChunkTicketManagerInterface.java @@ -0,0 +1,16 @@ +package carpet.fakes; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.Ticket; +import net.minecraft.util.SortedArraySet; +import net.minecraft.world.level.ChunkPos; + +public interface ChunkTicketManagerInterface +{ + void changeSpawnChunks(ChunkPos pos, int distance); + + Long2ObjectOpenHashMap>> getTicketsByPosition(); + + void replaceHolder(ChunkHolder oldHolder, ChunkHolder newHolder); +} diff --git a/src/main/java/carpet/fakes/ClientConnectionInterface.java b/src/main/java/carpet/fakes/ClientConnectionInterface.java new file mode 100644 index 0000000..127f3ac --- /dev/null +++ b/src/main/java/carpet/fakes/ClientConnectionInterface.java @@ -0,0 +1,7 @@ +package carpet.fakes; + +import io.netty.channel.Channel; + +public interface ClientConnectionInterface { + void setChannel(Channel channel); +} diff --git a/src/main/java/carpet/fakes/CommandDispatcherInterface.java b/src/main/java/carpet/fakes/CommandDispatcherInterface.java new file mode 100644 index 0000000..473b638 --- /dev/null +++ b/src/main/java/carpet/fakes/CommandDispatcherInterface.java @@ -0,0 +1,5 @@ +package carpet.fakes; + +public interface CommandDispatcherInterface { + void carpet$unregister(String node); +} diff --git a/src/main/java/carpet/fakes/CommandNodeInterface.java b/src/main/java/carpet/fakes/CommandNodeInterface.java new file mode 100644 index 0000000..9401dfb --- /dev/null +++ b/src/main/java/carpet/fakes/CommandNodeInterface.java @@ -0,0 +1,5 @@ +package carpet.fakes; + +public interface CommandNodeInterface { + void carpet$removeChild(String name); +} diff --git a/src/main/java/carpet/fakes/CoralFeatureInterface.java b/src/main/java/carpet/fakes/CoralFeatureInterface.java new file mode 100644 index 0000000..915d09b --- /dev/null +++ b/src/main/java/carpet/fakes/CoralFeatureInterface.java @@ -0,0 +1,12 @@ +package carpet.fakes; + +import java.util.Random; +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +public interface CoralFeatureInterface +{ + boolean growSpecific(Level worldIn, RandomSource random, BlockPos pos, BlockState blockUnder); +} diff --git a/src/main/java/carpet/fakes/EntityInterface.java b/src/main/java/carpet/fakes/EntityInterface.java new file mode 100644 index 0000000..80093ed --- /dev/null +++ b/src/main/java/carpet/fakes/EntityInterface.java @@ -0,0 +1,22 @@ +package carpet.fakes; + +import carpet.script.EntityEventsGroup; + +public interface EntityInterface +{ + float getMainYaw(float partialTicks); + + EntityEventsGroup getEventContainer(); + + boolean isPermanentVehicle(); + + void setPermanentVehicle(boolean permanent); + + int getPortalTimer(); + + void setPortalTimer(int amount); + + int getPublicNetherPortalCooldown(); + void setPublicNetherPortalCooldown(int what); + +} diff --git a/src/main/java/carpet/fakes/IngredientInterface.java b/src/main/java/carpet/fakes/IngredientInterface.java new file mode 100644 index 0000000..f6d9385 --- /dev/null +++ b/src/main/java/carpet/fakes/IngredientInterface.java @@ -0,0 +1,14 @@ +package carpet.fakes; + +import java.util.Collection; +import java.util.List; +import net.minecraft.world.item.ItemStack; + +public interface IngredientInterface +{ + /** + * Gets all the stacks of the ingredients for a given item recipe. Also used for {@link carpet.helpers.HopperCounter#guessColor} + * to guess the colour of an item to display it prettily + */ + List> getRecipeStacks(); +} diff --git a/src/main/java/carpet/fakes/InventoryBearerInterface.java b/src/main/java/carpet/fakes/InventoryBearerInterface.java new file mode 100644 index 0000000..d40a02e --- /dev/null +++ b/src/main/java/carpet/fakes/InventoryBearerInterface.java @@ -0,0 +1,8 @@ +package carpet.fakes; + +import net.minecraft.world.Container; + +public interface InventoryBearerInterface +{ + Container getCMInventory(); // can be removed in 1.17 due to class_6067 +} diff --git a/src/main/java/carpet/fakes/ItemEntityInterface.java b/src/main/java/carpet/fakes/ItemEntityInterface.java new file mode 100644 index 0000000..4534bbf --- /dev/null +++ b/src/main/java/carpet/fakes/ItemEntityInterface.java @@ -0,0 +1,6 @@ +package carpet.fakes; + +public interface ItemEntityInterface +{ + int getPickupDelayCM(); +} diff --git a/src/main/java/carpet/fakes/LevelInterface.java b/src/main/java/carpet/fakes/LevelInterface.java new file mode 100644 index 0000000..583da75 --- /dev/null +++ b/src/main/java/carpet/fakes/LevelInterface.java @@ -0,0 +1,25 @@ +package carpet.fakes; + +import net.minecraft.world.level.redstone.NeighborUpdater; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +public interface LevelInterface +{ + Map, Entity> getPrecookedMobs(); + + boolean setBlockStateWithBlockEntity(BlockPos blockPos, BlockState blockState, BlockEntity newBlockEntity, int int1); + + List getOtherEntitiesLimited(@Nullable Entity except, AABB box, Predicate predicate, int limit); + + NeighborUpdater getNeighborUpdater(); +} diff --git a/src/main/java/carpet/fakes/Lighting_scarpetChunkCreationInterface.java b/src/main/java/carpet/fakes/Lighting_scarpetChunkCreationInterface.java new file mode 100644 index 0000000..7cfdb5f --- /dev/null +++ b/src/main/java/carpet/fakes/Lighting_scarpetChunkCreationInterface.java @@ -0,0 +1,6 @@ +package carpet.fakes; + +public interface Lighting_scarpetChunkCreationInterface +{ + void removeLightData(long pos); +} diff --git a/src/main/java/carpet/fakes/LivingEntityInterface.java b/src/main/java/carpet/fakes/LivingEntityInterface.java new file mode 100644 index 0000000..2dba302 --- /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/MinecraftServerInterface.java b/src/main/java/carpet/fakes/MinecraftServerInterface.java new file mode 100644 index 0000000..5329042 --- /dev/null +++ b/src/main/java/carpet/fakes/MinecraftServerInterface.java @@ -0,0 +1,24 @@ +package carpet.fakes; + +import java.util.Map; +import java.util.function.BooleanSupplier; +import carpet.script.CarpetScriptServer; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.LevelStorageSource; + +public interface MinecraftServerInterface +{ + void forceTick(BooleanSupplier sup); + LevelStorageSource.LevelStorageAccess getCMSession(); + Map, ServerLevel> getCMWorlds(); + void reloadAfterReload(RegistryAccess newRegs); + + MinecraftServer.ReloadableResources getResourceManager(); + + void addScriptServer(CarpetScriptServer scriptServer); + CarpetScriptServer getScriptServer(); +} diff --git a/src/main/java/carpet/fakes/MobEntityInterface.java b/src/main/java/carpet/fakes/MobEntityInterface.java new file mode 100644 index 0000000..df0dd02 --- /dev/null +++ b/src/main/java/carpet/fakes/MobEntityInterface.java @@ -0,0 +1,14 @@ +package carpet.fakes; + +import java.util.Map; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.goal.GoalSelector; + +public interface MobEntityInterface +{ + GoalSelector getAI(boolean target); + + Map getTemporaryTasks(); + + void setPersistence(boolean what); +} diff --git a/src/main/java/carpet/fakes/PistonBlockEntityInterface.java b/src/main/java/carpet/fakes/PistonBlockEntityInterface.java new file mode 100644 index 0000000..247fbaa --- /dev/null +++ b/src/main/java/carpet/fakes/PistonBlockEntityInterface.java @@ -0,0 +1,12 @@ +package carpet.fakes; + +import net.minecraft.world.level.block.entity.BlockEntity; + +public interface PistonBlockEntityInterface +{ + void setCarriedBlockEntity(BlockEntity blockEntity); + BlockEntity getCarriedBlockEntity(); + void setRenderCarriedBlockEntity(boolean b); + boolean getRenderCarriedBlockEntity(); + boolean isRenderModeSet(); +} diff --git a/src/main/java/carpet/fakes/PistonBlockInterface.java b/src/main/java/carpet/fakes/PistonBlockInterface.java new file mode 100644 index 0000000..dcda174 --- /dev/null +++ b/src/main/java/carpet/fakes/PistonBlockInterface.java @@ -0,0 +1,10 @@ +package carpet.fakes; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +public interface PistonBlockInterface +{ + boolean publicShouldExtend(Level world_1, BlockPos blockPos_1, Direction direction_1); +} diff --git a/src/main/java/carpet/fakes/PlayerListHudInterface.java b/src/main/java/carpet/fakes/PlayerListHudInterface.java new file mode 100644 index 0000000..058ae90 --- /dev/null +++ b/src/main/java/carpet/fakes/PlayerListHudInterface.java @@ -0,0 +1,6 @@ +package carpet.fakes; + +public interface PlayerListHudInterface +{ + boolean hasFooterOrHeader(); +} diff --git a/src/main/java/carpet/fakes/PortalProcessorInterface.java b/src/main/java/carpet/fakes/PortalProcessorInterface.java new file mode 100644 index 0000000..284798e --- /dev/null +++ b/src/main/java/carpet/fakes/PortalProcessorInterface.java @@ -0,0 +1,6 @@ +package carpet.fakes; + +public interface PortalProcessorInterface +{ + void setPortalTime(int time); +} diff --git a/src/main/java/carpet/fakes/RandomStateVisitorAccessor.java b/src/main/java/carpet/fakes/RandomStateVisitorAccessor.java new file mode 100644 index 0000000..d423bb5 --- /dev/null +++ b/src/main/java/carpet/fakes/RandomStateVisitorAccessor.java @@ -0,0 +1,7 @@ +package carpet.fakes; + +import net.minecraft.world.level.levelgen.DensityFunction; + +public interface RandomStateVisitorAccessor { + DensityFunction.Visitor getVisitor(); +} diff --git a/src/main/java/carpet/fakes/RecipeManagerInterface.java b/src/main/java/carpet/fakes/RecipeManagerInterface.java new file mode 100644 index 0000000..78cfe42 --- /dev/null +++ b/src/main/java/carpet/fakes/RecipeManagerInterface.java @@ -0,0 +1,17 @@ +package carpet.fakes; + +import java.util.List; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; + +public interface RecipeManagerInterface +{ + /** + * Gets all the recipes for a given item. Also used for {@link carpet.helpers.HopperCounter#guessColor} to guess the + * colour of an item to display it prettily + */ + List> getAllMatching(final RecipeType type, final ResourceLocation itemId, final RegistryAccess registryAccess); +} diff --git a/src/main/java/carpet/fakes/RedstoneWireBlockInterface.java b/src/main/java/carpet/fakes/RedstoneWireBlockInterface.java new file mode 100644 index 0000000..8d6b771 --- /dev/null +++ b/src/main/java/carpet/fakes/RedstoneWireBlockInterface.java @@ -0,0 +1,11 @@ +package carpet.fakes; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +public interface RedstoneWireBlockInterface { + BlockState updateLogicPublic(Level world_1, BlockPos blockPos_1, BlockState blockState_1); + void setWiresGivePower(boolean wiresGivePower); + boolean getWiresGivePower(); +} diff --git a/src/main/java/carpet/fakes/ServerGamePacketListenerImplInterface.java b/src/main/java/carpet/fakes/ServerGamePacketListenerImplInterface.java new file mode 100644 index 0000000..fbd0f83 --- /dev/null +++ b/src/main/java/carpet/fakes/ServerGamePacketListenerImplInterface.java @@ -0,0 +1,7 @@ +package carpet.fakes; + +import net.minecraft.network.Connection; + +public interface ServerGamePacketListenerImplInterface { + Connection getConnection(); +} diff --git a/src/main/java/carpet/fakes/ServerLightingProviderInterface.java b/src/main/java/carpet/fakes/ServerLightingProviderInterface.java new file mode 100644 index 0000000..780c21b --- /dev/null +++ b/src/main/java/carpet/fakes/ServerLightingProviderInterface.java @@ -0,0 +1,16 @@ +package carpet.fakes; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; + +public interface ServerLightingProviderInterface +{ + void invokeUpdateChunkStatus(ChunkPos pos); + + void removeLightData(ChunkAccess chunk); + + CompletableFuture relight(ChunkAccess chunk); + + void resetLight(ChunkAccess chunk, ChunkPos pos); +} diff --git a/src/main/java/carpet/fakes/ServerPlayerInteractionManagerInterface.java b/src/main/java/carpet/fakes/ServerPlayerInteractionManagerInterface.java new file mode 100644 index 0000000..6fcf2a0 --- /dev/null +++ b/src/main/java/carpet/fakes/ServerPlayerInteractionManagerInterface.java @@ -0,0 +1,12 @@ +package carpet.fakes; + +import net.minecraft.core.BlockPos; + +public interface ServerPlayerInteractionManagerInterface +{ + BlockPos getCurrentBreakingBlock(); + + int getCurrentBlockBreakingProgress(); + + void setBlockBreakingProgress(int progress); +} diff --git a/src/main/java/carpet/fakes/ServerPlayerInterface.java b/src/main/java/carpet/fakes/ServerPlayerInterface.java new file mode 100644 index 0000000..96c8e09 --- /dev/null +++ b/src/main/java/carpet/fakes/ServerPlayerInterface.java @@ -0,0 +1,10 @@ +package carpet.fakes; + +import carpet.helpers.EntityPlayerActionPack; + +public interface ServerPlayerInterface +{ + EntityPlayerActionPack getActionPack(); + void invalidateEntityObjectReference(); + boolean isInvalidEntityObject(); +} diff --git a/src/main/java/carpet/fakes/ServerWorldInterface.java b/src/main/java/carpet/fakes/ServerWorldInterface.java new file mode 100644 index 0000000..8617aca --- /dev/null +++ b/src/main/java/carpet/fakes/ServerWorldInterface.java @@ -0,0 +1,10 @@ +package carpet.fakes; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.storage.ServerLevelData; + +public interface ServerWorldInterface { + ServerLevelData getWorldPropertiesCM(); + LevelEntityGetter getEntityLookupCMPublic(); +} diff --git a/src/main/java/carpet/fakes/SimpleEntityLookupInterface.java b/src/main/java/carpet/fakes/SimpleEntityLookupInterface.java new file mode 100644 index 0000000..415fc31 --- /dev/null +++ b/src/main/java/carpet/fakes/SimpleEntityLookupInterface.java @@ -0,0 +1,10 @@ +package carpet.fakes; + +import java.util.List; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.entity.EntityAccess; + +public interface SimpleEntityLookupInterface +{ + List getChunkEntities(ChunkPos chpos); +} diff --git a/src/main/java/carpet/fakes/SpawnGroupInterface.java b/src/main/java/carpet/fakes/SpawnGroupInterface.java new file mode 100644 index 0000000..6f28807 --- /dev/null +++ b/src/main/java/carpet/fakes/SpawnGroupInterface.java @@ -0,0 +1,6 @@ +package carpet.fakes; + +public interface SpawnGroupInterface +{ + int getInitialSpawnCap(); +} diff --git a/src/main/java/carpet/fakes/SpawnHelperInnerInterface.java b/src/main/java/carpet/fakes/SpawnHelperInnerInterface.java new file mode 100644 index 0000000..6736fcb --- /dev/null +++ b/src/main/java/carpet/fakes/SpawnHelperInnerInterface.java @@ -0,0 +1,8 @@ +package carpet.fakes; + +import net.minecraft.world.level.PotentialCalculator; + +public interface SpawnHelperInnerInterface +{ + PotentialCalculator getPotentialCalculator(); +} diff --git a/src/main/java/carpet/fakes/ThreadedAnvilChunkStorageInterface.java b/src/main/java/carpet/fakes/ThreadedAnvilChunkStorageInterface.java new file mode 100644 index 0000000..a46c128 --- /dev/null +++ b/src/main/java/carpet/fakes/ThreadedAnvilChunkStorageInterface.java @@ -0,0 +1,17 @@ +package carpet.fakes; + +import java.util.List; +import java.util.Map; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.world.level.ChunkPos; + +public interface ThreadedAnvilChunkStorageInterface +{ + //Map regenerateChunkRegion(List requestedChunks); + + //void relightChunk(ChunkPos pos); + + //void releaseRelightTicket(ChunkPos pos); + + //Iterable getChunksCM(); +} diff --git a/src/main/java/carpet/fakes/TntEntityInterface.java b/src/main/java/carpet/fakes/TntEntityInterface.java new file mode 100644 index 0000000..b509324 --- /dev/null +++ b/src/main/java/carpet/fakes/TntEntityInterface.java @@ -0,0 +1,6 @@ +package carpet.fakes; + +public interface TntEntityInterface +{ + int getMergedTNT(); +} diff --git a/src/main/java/carpet/fakes/WorldChunkInterface.java b/src/main/java/carpet/fakes/WorldChunkInterface.java new file mode 100644 index 0000000..d93b0a1 --- /dev/null +++ b/src/main/java/carpet/fakes/WorldChunkInterface.java @@ -0,0 +1,10 @@ +package carpet.fakes; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +public interface WorldChunkInterface +{ + BlockState setBlockStateWithBlockEntity(BlockPos blockPos, BlockState newBlockState, BlockEntity newBlockEntity, boolean boolean1); +} diff --git a/src/main/java/carpet/helpers/BlockRotator.java b/src/main/java/carpet/helpers/BlockRotator.java new file mode 100644 index 0000000..a815c8d --- /dev/null +++ b/src/main/java/carpet/helpers/BlockRotator.java @@ -0,0 +1,206 @@ +package carpet.helpers; + +import carpet.fakes.PistonBlockInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.dispenser.BlockSource; +import net.minecraft.core.dispenser.DispenseItemBehavior; +import net.minecraft.core.dispenser.OptionalDispenseItemBehavior; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseRailBlock; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.DirectionalBlock; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.block.EndRodBlock; +import net.minecraft.world.level.block.HopperBlock; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.ObserverBlock; +import net.minecraft.world.level.block.RotatedPillarBlock; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.SlabBlock; +import net.minecraft.world.level.block.StairBlock; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import net.minecraft.world.level.block.piston.PistonStructureResolver; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import carpet.CarpetSettings; + +public class BlockRotator +{ + public static boolean flipBlockWithCactus(BlockState state, Level world, Player player, InteractionHand hand, BlockHitResult hit) + { + if (!player.getAbilities().mayBuild || !CarpetSettings.flippinCactus || !playerHoldsCactusMainhand(player)) + { + return false; + } + CarpetSettings.impendingFillSkipUpdates.set(true); + boolean retval = flipBlock(state, world, player, hand, hit); + CarpetSettings.impendingFillSkipUpdates.set(false); + return retval; + } + + public static ItemStack dispenserRotate(BlockSource source, ItemStack stack) + { + Direction sourceFace = source.state().getValue(DispenserBlock.FACING); + Level world = source.level(); + BlockPos blockpos = source.pos().relative(sourceFace); // offset + BlockState blockstate = world.getBlockState(blockpos); + Block block = blockstate.getBlock(); + + // Block rotation for blocks that can be placed in all 6 or 4 rotations. + if (block instanceof DirectionalBlock || block instanceof DispenserBlock) + { + Direction face = blockstate.getValue(DirectionalBlock.FACING); + if (block instanceof PistonBaseBlock && ( + blockstate.getValue(PistonBaseBlock.EXTENDED) + || ( ((PistonBlockInterface)block).publicShouldExtend(world, blockpos, face) && (new PistonStructureResolver(world, blockpos, face, true)).resolve() ) + ) + ) + { + return stack; + } + + Direction rotatedFace = face.getClockWise(sourceFace.getAxis()); + if (sourceFace.get3DDataValue() % 2 == 0 || rotatedFace == face) + { // Flip to make blocks always rotate clockwise relative to the dispenser + // when index is equal to zero. when index is equal to zero the dispenser is in the opposite direction. + rotatedFace = rotatedFace.getOpposite(); + } + world.setBlock(blockpos, blockstate.setValue(DirectionalBlock.FACING, rotatedFace), 3); + } + else if (block instanceof HorizontalDirectionalBlock) // Block rotation for blocks that can be placed in only 4 horizontal rotations. + { + if (block instanceof BedBlock) + return stack; + Direction face = blockstate.getValue(HorizontalDirectionalBlock.FACING).getClockWise(Direction.Axis.Y); + + if (sourceFace == Direction.DOWN) + { // same as above. + face = face.getOpposite(); + } + world.setBlock(blockpos, blockstate.setValue(HorizontalDirectionalBlock.FACING, face), 3); + } + else if (block == Blocks.HOPPER) + { + Direction face = blockstate.getValue(HopperBlock.FACING); + if (face != Direction.DOWN) + { + face = face.getClockWise(Direction.Axis.Y); + world.setBlock(blockpos, blockstate.setValue(HopperBlock.FACING, face), 3); + } + } + // Send block update to the block that just have been rotated. + world.neighborChanged(blockpos, block, source.pos()); + + return stack; + } + + public static boolean flipBlock(BlockState state, Level world, Player player, InteractionHand hand, BlockHitResult hit) + { + Block block = state.getBlock(); + BlockPos pos = hit.getBlockPos(); + Vec3 hitVec = hit.getLocation().subtract(pos.getX(), pos.getY(), pos.getZ()); + Direction facing = hit.getDirection(); + BlockState newState = null; + if ((block instanceof HorizontalDirectionalBlock || block instanceof BaseRailBlock) && !(block instanceof BedBlock)) + { + newState = state.rotate(Rotation.CLOCKWISE_90); + } + else if (block instanceof ObserverBlock || block instanceof EndRodBlock) + { + newState = state.setValue(DirectionalBlock.FACING, state.getValue(DirectionalBlock.FACING).getOpposite()); + } + else if (block instanceof DispenserBlock) + { + newState = state.setValue(DispenserBlock.FACING, state.getValue(DispenserBlock.FACING).getOpposite()); + } + else if (block instanceof PistonBaseBlock) + { + if (!(state.getValue(PistonBaseBlock.EXTENDED))) + newState = state.setValue(DirectionalBlock.FACING, state.getValue(DirectionalBlock.FACING).getOpposite()); + } + else if (block instanceof SlabBlock) + { + if (state.getValue(SlabBlock.TYPE) != SlabType.DOUBLE) + { + newState = state.setValue(SlabBlock.TYPE, state.getValue(SlabBlock.TYPE) == SlabType.TOP ? SlabType.BOTTOM : SlabType.TOP); + } + } + else if (block instanceof HopperBlock) + { + if (state.getValue(HopperBlock.FACING) != Direction.DOWN) + { + newState = state.setValue(HopperBlock.FACING, state.getValue(HopperBlock.FACING).getClockWise()); + } + } + else if (block instanceof StairBlock) + { + if ((facing == Direction.UP && hitVec.y == 1.0f) || (facing == Direction.DOWN && hitVec.y == 0.0f)) + { + newState = state.setValue(StairBlock.HALF, state.getValue(StairBlock.HALF) == Half.TOP ? Half.BOTTOM : Half.TOP ); + } + else + { + boolean turnCounterClockwise = switch (facing) { + case NORTH -> (hitVec.x <= 0.5); + case SOUTH -> !(hitVec.x <= 0.5); + case EAST -> (hitVec.z <= 0.5); + case WEST -> !(hitVec.z <= 0.5); + default -> false; + }; + newState = state.rotate(turnCounterClockwise ? Rotation.COUNTERCLOCKWISE_90 : Rotation.CLOCKWISE_90); + } + } + else if (block instanceof RotatedPillarBlock) + { + newState = state.setValue(RotatedPillarBlock.AXIS, switch (state.getValue(RotatedPillarBlock.AXIS)) { + case X -> Direction.Axis.Z; + case Y -> Direction.Axis.X; + case Z -> Direction.Axis.Y; + }); + } + if (newState != null) + { + world.setBlock(pos, newState, Block.UPDATE_CLIENTS | 1024); // no constant matching 1024 in Block, what does this do? + world.setBlocksDirty(pos, state, newState); + return true; + } + return false; + } + + private static boolean playerHoldsCactusMainhand(Player playerIn) + { + return playerIn.getMainHandItem().getItem() == Items.CACTUS; + } + + public static boolean flippinEligibility(Entity entity) + { + return CarpetSettings.flippinCactus && entity instanceof Player p && p.getOffhandItem().getItem() == Items.CACTUS; + } + + public static class CactusDispenserBehaviour extends OptionalDispenseItemBehavior implements DispenseItemBehavior + { + @Override + protected ItemStack execute(BlockSource source, ItemStack stack) + { + if (CarpetSettings.rotatorBlock) + { + return BlockRotator.dispenserRotate(source, stack); + } + else + { + return super.execute(source, stack); + } + } + } +} diff --git a/src/main/java/carpet/helpers/CarpetTaintedList.java b/src/main/java/carpet/helpers/CarpetTaintedList.java new file mode 100644 index 0000000..5ab22b1 --- /dev/null +++ b/src/main/java/carpet/helpers/CarpetTaintedList.java @@ -0,0 +1,12 @@ +package carpet.helpers; + +import java.util.ArrayList; +import java.util.List; + +public class CarpetTaintedList extends ArrayList +{ + public CarpetTaintedList(final List list) + { + super(list); + } +} diff --git a/src/main/java/carpet/helpers/EntityPlayerActionPack.java b/src/main/java/carpet/helpers/EntityPlayerActionPack.java new file mode 100644 index 0000000..93bafba --- /dev/null +++ b/src/main/java/carpet/helpers/EntityPlayerActionPack.java @@ -0,0 +1,645 @@ +package carpet.helpers; + +import carpet.fakes.ServerPlayerInterface; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import carpet.patches.EntityPlayerMPFake; +import carpet.script.utils.Tracer; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.vehicle.Boat; +import net.minecraft.world.entity.vehicle.Minecart; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; + +public class EntityPlayerActionPack +{ + private final ServerPlayer player; + + private final Map actions = new EnumMap<>(ActionType.class); + + private BlockPos currentBlock; + private int blockHitDelay; + private boolean isHittingBlock; + private float curBlockDamageMP; + + private boolean sneaking; + private boolean sprinting; + private float forward; + private float strafing; + + private int itemUseCooldown; + + public EntityPlayerActionPack(ServerPlayer playerIn) + { + player = playerIn; + stopAll(); + } + public void copyFrom(EntityPlayerActionPack other) + { + actions.putAll(other.actions); + currentBlock = other.currentBlock; + blockHitDelay = other.blockHitDelay; + isHittingBlock = other.isHittingBlock; + curBlockDamageMP = other.curBlockDamageMP; + + sneaking = other.sneaking; + sprinting = other.sprinting; + forward = other.forward; + strafing = other.strafing; + + itemUseCooldown = other.itemUseCooldown; + } + + public EntityPlayerActionPack start(ActionType type, Action action) + { + if (action.isContinuous) + { + Action current = actions.get(type); + if (current != null) return this; + } + + Action previous = actions.remove(type); + if (previous != null) type.stop(player, previous); + if (action != null) + { + actions.put(type, action); + type.start(player, action); // noop + } + return this; + } + + public EntityPlayerActionPack setSneaking(boolean doSneak) + { + sneaking = doSneak; + player.setShiftKeyDown(doSneak); +// if (sprinting && sneaking) +// setSprinting(false); + return this; + } + public EntityPlayerActionPack setSprinting(boolean doSprint) + { + sprinting = doSprint; + player.setSprinting(doSprint); +// if (sneaking && sprinting) +// setSneaking(false); + return this; + } + + public EntityPlayerActionPack setForward(float value) + { + forward = value; + return this; + } + public EntityPlayerActionPack setStrafing(float value) + { + strafing = value; + return this; + } + public EntityPlayerActionPack look(Direction direction) + { + return switch (direction) + { + case NORTH -> look(180, 0); + case SOUTH -> look(0, 0); + case EAST -> look(-90, 0); + case WEST -> look(90, 0); + case UP -> look(player.getYRot(), -90); + case DOWN -> look(player.getYRot(), 90); + }; + } + public EntityPlayerActionPack look(Vec2 rotation) + { + return look(rotation.x, rotation.y); + } + + public EntityPlayerActionPack look(float yaw, float pitch) + { + player.setYRot(yaw % 360); //setYaw + player.setXRot(Mth.clamp(pitch, -90, 90)); // setPitch + // maybe player.moveTo(player.getX(), player.getY(), player.getZ(), yaw, Mth.clamp(pitch,-90.0F, 90.0F)); + return this; + } + + public EntityPlayerActionPack lookAt(Vec3 position) + { + player.lookAt(EntityAnchorArgument.Anchor.EYES, position); + return this; + } + + public EntityPlayerActionPack turn(float yaw, float pitch) + { + return look(player.getYRot() + yaw, player.getXRot() + pitch); + } + + public EntityPlayerActionPack turn(Vec2 rotation) + { + return turn(rotation.x, rotation.y); + } + + public EntityPlayerActionPack stopMovement() + { + setSneaking(false); + setSprinting(false); + forward = 0.0F; + strafing = 0.0F; + return this; + } + + + public EntityPlayerActionPack stopAll() + { + for (ActionType type : actions.keySet()) type.stop(player, actions.get(type)); + actions.clear(); + return stopMovement(); + } + + public EntityPlayerActionPack mount(boolean onlyRideables) + { + //test what happens + List entities; + if (onlyRideables) + { + entities = player.level().getEntities(player, player.getBoundingBox().inflate(3.0D, 1.0D, 3.0D), + e -> e instanceof Minecart || e instanceof Boat || e instanceof AbstractHorse); + } + else + { + entities = player.level().getEntities(player, player.getBoundingBox().inflate(3.0D, 1.0D, 3.0D)); + } + if (entities.size()==0) + return this; + Entity closest = null; + double distance = Double.POSITIVE_INFINITY; + Entity currentVehicle = player.getVehicle(); + for (Entity e: entities) + { + if (e == player || (currentVehicle == e)) + continue; + double dd = player.distanceToSqr(e); + if (dd actionAttempts = new HashMap<>(); + actions.values().removeIf(e -> e.done); + for (Map.Entry e : actions.entrySet()) + { + ActionType type = e.getKey(); + Action action = e.getValue(); + // skipping attack if use was successful + if (!(actionAttempts.getOrDefault(ActionType.USE, false) && type == ActionType.ATTACK)) + { + Boolean actionStatus = action.tick(this, type); + if (actionStatus != null) + actionAttempts.put(type, actionStatus); + } + // optionally retrying use after successful attack and unsuccessful use + if (type == ActionType.ATTACK + && actionAttempts.getOrDefault(ActionType.ATTACK, false) + && !actionAttempts.getOrDefault(ActionType.USE, true) ) + { + // according to MinecraftClient.handleInputEvents + Action using = actions.get(ActionType.USE); + if (using != null) // this is always true - we know use worked, but just in case + { + using.retry(this, ActionType.USE); + } + } + } + + float vel = sneaking?0.30F:1.0F; + vel *= player.isUsingItem()?0.20F:1.0F; + + // The != 0.0F checks are needed given else real players can't control minecarts, however it works with fakes and else they don't stop immediately + if (forward != 0.0F || player instanceof EntityPlayerMPFake) { + player.zza = forward * vel; + } + if (strafing != 0.0F || player instanceof EntityPlayerMPFake) { + player.xxa = strafing * vel; + } + } + + static HitResult getTarget(ServerPlayer player) + { + double blockReach = player.gameMode.isCreative() ? 5 : 4.5f; + double entityReach = player.gameMode.isCreative() ? 5 : 3f; + + HitResult hit = Tracer.rayTrace(player, 1, blockReach, false); + + if (hit.getType() == HitResult.Type.BLOCK) return hit; + return Tracer.rayTrace(player, 1, entityReach, false); + } + + private void dropItemFromSlot(int slot, boolean dropAll) + { + Inventory inv = player.getInventory(); // getInventory; + if (!inv.getItem(slot).isEmpty()) + player.drop(inv.removeItem(slot, + dropAll ? inv.getItem(slot).getCount() : 1 + ), false, true); // scatter, keep owner + } + + public void drop(int selectedSlot, boolean dropAll) + { + Inventory inv = player.getInventory(); // getInventory; + if (selectedSlot == -2) // all + { + for (int i = inv.getContainerSize(); i >= 0; i--) + dropItemFromSlot(i, dropAll); + } + else // one slot + { + if (selectedSlot == -1) + selectedSlot = inv.selected; + dropItemFromSlot(selectedSlot, dropAll); + } + } + + public void setSlot(int slot) + { + player.getInventory().selected = slot-1; + player.connection.send(new ClientboundSetCarriedItemPacket(slot-1)); + } + + public enum ActionType + { + USE(true) + { + @Override + boolean execute(ServerPlayer player, Action action) + { + EntityPlayerActionPack ap = ((ServerPlayerInterface) player).getActionPack(); + if (ap.itemUseCooldown > 0) + { + ap.itemUseCooldown--; + return true; + } + if (player.isUsingItem()) + { + return true; + } + HitResult hit = getTarget(player); + for (InteractionHand hand : InteractionHand.values()) + { + switch (hit.getType()) + { + case BLOCK: + { + player.resetLastActionTime(); + ServerLevel world = player.serverLevel(); + BlockHitResult blockHit = (BlockHitResult) hit; + BlockPos pos = blockHit.getBlockPos(); + Direction side = blockHit.getDirection(); + if (pos.getY() < player.level().getMaxBuildHeight() - (side == Direction.UP ? 1 : 0) && world.mayInteract(player, pos)) + { + InteractionResult result = player.gameMode.useItemOn(player, world, player.getItemInHand(hand), hand, blockHit); + player.swing(hand); + if (result.consumesAction()) + { + ap.itemUseCooldown = 3; + return true; + } + } + break; + } + case ENTITY: + { + player.resetLastActionTime(); + EntityHitResult entityHit = (EntityHitResult) hit; + Entity entity = entityHit.getEntity(); + boolean handWasEmpty = player.getItemInHand(hand).isEmpty(); + boolean itemFrameEmpty = (entity instanceof ItemFrame) && ((ItemFrame) entity).getItem().isEmpty(); + Vec3 relativeHitPos = entityHit.getLocation().subtract(entity.getX(), entity.getY(), entity.getZ()); + if (entity.interactAt(player, relativeHitPos, hand).consumesAction()) + { + ap.itemUseCooldown = 3; + return true; + } + // fix for SS itemframe always returns CONSUME even if no action is performed + if (player.interactOn(entity, hand).consumesAction() && !(handWasEmpty && itemFrameEmpty)) + { + ap.itemUseCooldown = 3; + return true; + } + break; + } + } + ItemStack handItem = player.getItemInHand(hand); + if (player.gameMode.useItem(player, player.level(), handItem, hand).consumesAction()) + { + ap.itemUseCooldown = 3; + return true; + } + } + return false; + } + + @Override + void inactiveTick(ServerPlayer player, Action action) + { + EntityPlayerActionPack ap = ((ServerPlayerInterface) player).getActionPack(); + ap.itemUseCooldown = 0; + player.releaseUsingItem(); + } + }, + ATTACK(true) { + @Override + boolean execute(ServerPlayer player, Action action) { + HitResult hit = getTarget(player); + switch (hit.getType()) { + case ENTITY: { + EntityHitResult entityHit = (EntityHitResult) hit; + if (!action.isContinuous) + { + player.attack(entityHit.getEntity()); + player.swing(InteractionHand.MAIN_HAND); + } + player.resetAttackStrengthTicker(); + player.resetLastActionTime(); + return true; + } + case BLOCK: { + EntityPlayerActionPack ap = ((ServerPlayerInterface) player).getActionPack(); + if (ap.blockHitDelay > 0) + { + ap.blockHitDelay--; + return false; + } + BlockHitResult blockHit = (BlockHitResult) hit; + BlockPos pos = blockHit.getBlockPos(); + Direction side = blockHit.getDirection(); + if (player.blockActionRestricted(player.level(), pos, player.gameMode.getGameModeForPlayer())) return false; + if (ap.currentBlock != null && player.level().getBlockState(ap.currentBlock).isAir()) + { + ap.currentBlock = null; + return false; + } + BlockState state = player.level().getBlockState(pos); + boolean blockBroken = false; + if (player.gameMode.getGameModeForPlayer().isCreative()) + { + player.gameMode.handleBlockBreakAction(pos, ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, side, player.level().getMaxBuildHeight(), -1); + ap.blockHitDelay = 5; + blockBroken = true; + } + else if (ap.currentBlock == null || !ap.currentBlock.equals(pos)) + { + if (ap.currentBlock != null) + { + player.gameMode.handleBlockBreakAction(ap.currentBlock, ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK, side, player.level().getMaxBuildHeight(), -1); + } + player.gameMode.handleBlockBreakAction(pos, ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK, side, player.level().getMaxBuildHeight(), -1); + boolean notAir = !state.isAir(); + if (notAir && ap.curBlockDamageMP == 0) + { + state.attack(player.level(), pos, player); + } + if (notAir && state.getDestroyProgress(player, player.level(), pos) >= 1) + { + ap.currentBlock = null; + //instamine?? + blockBroken = true; + } + else + { + ap.currentBlock = pos; + ap.curBlockDamageMP = 0; + } + } + else + { + ap.curBlockDamageMP += state.getDestroyProgress(player, player.level(), pos); + if (ap.curBlockDamageMP >= 1) + { + player.gameMode.handleBlockBreakAction(pos, ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK, side, player.level().getMaxBuildHeight(), -1); + ap.currentBlock = null; + ap.blockHitDelay = 5; + blockBroken = true; + } + player.level().destroyBlockProgress(-1, pos, (int) (ap.curBlockDamageMP * 10)); + + } + player.resetLastActionTime(); + player.swing(InteractionHand.MAIN_HAND); + return blockBroken; + } + } + if (!action.isContinuous) player.swing(InteractionHand.MAIN_HAND); + player.resetAttackStrengthTicker(); + player.resetLastActionTime(); + return false; + } + + @Override + void inactiveTick(ServerPlayer player, Action action) + { + EntityPlayerActionPack ap = ((ServerPlayerInterface) player).getActionPack(); + if (ap.currentBlock == null) return; + player.level().destroyBlockProgress(-1, ap.currentBlock, -1); + player.gameMode.handleBlockBreakAction(ap.currentBlock, ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK, Direction.DOWN, player.level().getMaxBuildHeight(), -1); + ap.currentBlock = null; + } + }, + JUMP(true) + { + @Override + boolean execute(ServerPlayer player, Action action) + { + if (action.limit == 1) + { + if (player.onGround()) player.jumpFromGround(); // onGround + } + else + { + player.setJumping(true); + } + return false; + } + + @Override + void inactiveTick(ServerPlayer player, Action action) + { + player.setJumping(false); + } + }, + DROP_ITEM(true) + { + @Override + boolean execute(ServerPlayer player, Action action) + { + player.resetLastActionTime(); + player.drop(false); // dropSelectedItem + return false; + } + }, + DROP_STACK(true) + { + @Override + boolean execute(ServerPlayer player, Action action) + { + player.resetLastActionTime(); + player.drop(true); // dropSelectedItem + return false; + } + }, + SWAP_HANDS(true) + { + @Override + boolean execute(ServerPlayer player, Action action) + { + player.resetLastActionTime(); + ItemStack itemStack_1 = player.getItemInHand(InteractionHand.OFF_HAND); + player.setItemInHand(InteractionHand.OFF_HAND, player.getItemInHand(InteractionHand.MAIN_HAND)); + player.setItemInHand(InteractionHand.MAIN_HAND, itemStack_1); + return false; + } + }; + + public final boolean preventSpectator; + + ActionType(boolean preventSpectator) + { + this.preventSpectator = preventSpectator; + } + + void start(ServerPlayer player, Action action) {} + abstract boolean execute(ServerPlayer player, Action action); + void inactiveTick(ServerPlayer player, Action action) {} + void stop(ServerPlayer player, Action action) + { + inactiveTick(player, action); + } + } + + public static class Action + { + public boolean done = false; + public final int limit; + public final int interval; + public final int offset; + private int count; + private int next; + private final boolean isContinuous; + + private Action(int limit, int interval, int offset, boolean continuous) + { + this.limit = limit; + this.interval = interval; + this.offset = offset; + next = interval + offset; + isContinuous = continuous; + } + + public static Action once() + { + return new Action(1, 1, 0, false); + } + + public static Action continuous() + { + return new Action(-1, 1, 0, true); + } + + public static Action interval(int interval) + { + return new Action(-1, interval, 0, false); + } + + public static Action interval(int interval, int offset) + { + return new Action(-1, interval, offset, false); + } + + Boolean tick(EntityPlayerActionPack actionPack, ActionType type) + { + next--; + Boolean cancel = null; + if (next <= 0) + { + if (interval == 1 && !isContinuous) + { + // need to allow entity to tick, otherwise won't have effect (bow) + // actions are 20 tps, so need to clear status mid tick, allowing entities process it till next time + if (!type.preventSpectator || !actionPack.player.isSpectator()) + { + type.inactiveTick(actionPack.player, this); + } + } + + if (!type.preventSpectator || !actionPack.player.isSpectator()) + { + cancel = type.execute(actionPack.player, this); + } + count++; + if (count == limit) + { + type.stop(actionPack.player, null); + done = true; + return cancel; + } + next = interval; + } + else + { + if (!type.preventSpectator || !actionPack.player.isSpectator()) + { + type.inactiveTick(actionPack.player, this); + } + } + return cancel; + } + + void retry(EntityPlayerActionPack actionPack, ActionType type) + { + //assuming action run but was unsuccesful that tick, but opportunity emerged to retry it, lets retry it. + if (!type.preventSpectator || !actionPack.player.isSpectator()) + { + type.execute(actionPack.player, this); + } + count++; + if (count == limit) + { + type.stop(actionPack.player, null); + done = true; + } + } + } +} diff --git a/src/main/java/carpet/helpers/FertilizableCoral.java b/src/main/java/carpet/helpers/FertilizableCoral.java new file mode 100644 index 0000000..9c8bd43 --- /dev/null +++ b/src/main/java/carpet/helpers/FertilizableCoral.java @@ -0,0 +1,88 @@ +package carpet.helpers; + +import carpet.fakes.CoralFeatureInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.BaseCoralPlantTypeBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.BonemealableBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.CoralClawFeature; +import net.minecraft.world.level.levelgen.feature.CoralFeature; +import net.minecraft.world.level.levelgen.feature.CoralMushroomFeature; +import net.minecraft.world.level.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; +import net.minecraft.world.level.material.MapColor; + +/** + * Deduplicates logic for the different behaviors of the {@code renewableCoral} rule + */ +public interface FertilizableCoral extends BonemealableBlock { + /** + * @return Whether the rule for this behavior is enabled + */ + boolean isEnabled(); + + @Override + public default boolean isValidBonemealTarget(LevelReader world, BlockPos pos, BlockState state) + { + return isEnabled() + && state.getValue(BaseCoralPlantTypeBlock.WATERLOGGED) + && world.getFluidState(pos.above()).is(FluidTags.WATER); + } + + @Override + public default boolean isBonemealSuccess(Level world, RandomSource random, BlockPos pos, BlockState state) + { + return random.nextFloat() < 0.15D; + } + + @Override + public default void performBonemeal(ServerLevel worldIn, RandomSource random, BlockPos pos, BlockState blockUnder) + { + int variant = random.nextInt(3); + CoralFeature coral = switch (variant) { + case 0 -> new CoralClawFeature(NoneFeatureConfiguration.CODEC); + case 1 -> new CoralTreeFeature(NoneFeatureConfiguration.CODEC); + default -> new CoralMushroomFeature(NoneFeatureConfiguration.CODEC); + }; + + MapColor color = blockUnder.getMapColor(worldIn, pos); + BlockState properBlock = blockUnder; + HolderSet.Named coralBlocks = worldIn.registryAccess().registryOrThrow(Registries.BLOCK).getTag(BlockTags.CORAL_BLOCKS).orElseThrow(); + for (Holder block: coralBlocks) + { + properBlock = block.value().defaultBlockState(); + if (properBlock.getMapColor(worldIn, pos) == color) + { + break; + } + } + worldIn.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_NONE); + + if (!((CoralFeatureInterface)coral).growSpecific(worldIn, random, pos, properBlock)) + { + worldIn.setBlock(pos, blockUnder, 3); + } + else + { + if (worldIn.random.nextInt(10) == 0) + { + BlockPos randomPos = pos.offset(worldIn.random.nextInt(16) - 8, worldIn.random.nextInt(8), worldIn.random.nextInt(16) - 8); + if (coralBlocks.contains(worldIn.getBlockState(randomPos).getBlockHolder())) + { + worldIn.setBlock(randomPos, Blocks.WET_SPONGE.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + } +} diff --git a/src/main/java/carpet/helpers/HopperCounter.java b/src/main/java/carpet/helpers/HopperCounter.java new file mode 100644 index 0000000..3e419e8 --- /dev/null +++ b/src/main/java/carpet/helpers/HopperCounter.java @@ -0,0 +1,426 @@ +package carpet.helpers; + +import carpet.CarpetServer; +import carpet.fakes.IngredientInterface; +import carpet.fakes.RecipeManagerInterface; +import carpet.utils.Messenger; +import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import net.minecraft.ChatFormatting; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.DyeItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.AbstractBannerBlock; +import net.minecraft.world.level.block.BeaconBeamBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.MapColor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Map.entry; + +/** + * The actual object residing in each hopper counter which makes them count the items and saves them. There is one for each + * colour in MC. + */ + +public class HopperCounter +{ + /** + * A map of all the {@link HopperCounter} counters. + */ + private static final Map COUNTERS; + + /** + * The default display colour of each item, which makes them look nicer when printing the counter contents to the chat + */ + + public static final TextColor WHITE = TextColor.fromLegacyFormat(ChatFormatting.WHITE); + + static + { + EnumMap counterMap = new EnumMap<>(DyeColor.class); + for (DyeColor color : DyeColor.values()) + { + counterMap.put(color, new HopperCounter(color)); + } + COUNTERS = Collections.unmodifiableMap(counterMap); + } + + /** + * The counter's colour, determined by the colour of wool it's pointing into + */ + public final DyeColor color; + /** + * The string which is passed into {@link Messenger#m} which makes each counter name be displayed in the colour of + * that counter. + */ + private final String coloredName; + /** + * All the items stored within the counter, as a map of {@link Item} mapped to a {@code long} of the amount of items + * stored thus far of that item type. + */ + private final Object2LongMap counter = new Object2LongLinkedOpenHashMap<>(); + /** + * The starting tick of the counter, used to calculate in-game time. Only initialised when the first item enters the + * counter + */ + private long startTick; + /** + * The starting millisecond of the counter, used to calculate IRl time. Only initialised when the first item enters + * the counter + */ + private long startMillis; + // private PubSubInfoProvider pubSubProvider; + + private HopperCounter(DyeColor color) + { + startTick = -1; + this.color = color; + String hexColor = Integer.toHexString(color.getTextColor()); + if (hexColor.length() < 6) + hexColor = "0".repeat(6 - hexColor.length()) + hexColor; + this.coloredName = '#' + hexColor + ' ' + color.getName(); + } + + /** + * Method used to add items to the counter. Note that this is when the {@link HopperCounter#startTick} and + * {@link HopperCounter#startMillis} variables are initialised, so you can place the counters and then start the farm + * after all the collection is sorted out. + */ + public void add(MinecraftServer server, ItemStack stack) + { + if (startTick < 0) + { + startTick = server.overworld().getGameTime(); + startMillis = System.currentTimeMillis(); + } + Item item = stack.getItem(); + counter.put(item, counter.getLong(item) + stack.getCount()); + // pubSubProvider.publish(); + } + + /** + * Resets the counter, clearing its items but keeping the clock running. + */ + public void reset(MinecraftServer server) + { + counter.clear(); + startTick = server.overworld().getGameTime(); + startMillis = System.currentTimeMillis(); + // pubSubProvider.publish(); + } + + /** + * Resets all counters, clearing their items. + * @param fresh Whether or not to start the clocks going immediately or later. + */ + public static void resetAll(MinecraftServer server, boolean fresh) + { + for (HopperCounter counter : COUNTERS.values()) + { + counter.reset(server); + if (fresh) counter.startTick = -1; + } + } + + /** + * Prints all the counters to chat, nicely formatted, and you can choose whether to diplay in in game time or IRL time + */ + public static List formatAll(MinecraftServer server, boolean realtime) + { + List text = new ArrayList<>(); + + for (HopperCounter counter : COUNTERS.values()) + { + List temp = counter.format(server, realtime, false); + if (temp.size() > 1) + { + if (!text.isEmpty()) text.add(Messenger.s("")); + text.addAll(temp); + } + } + if (text.isEmpty()) + { + text.add(Messenger.s("No items have been counted yet.")); + } + return text; + } + + /** + * Prints a single counter's contents and timings to chat, with the option to keep it short (so no item breakdown, + * only rates). Again, realtime displays IRL time as opposed to in game time. + */ + public List format(MinecraftServer server, boolean realTime, boolean brief) + { + long ticks = Math.max(realTime ? (System.currentTimeMillis() - startMillis) / 50 : server.overworld().getGameTime() - startTick, 1); + if (startTick < 0 || ticks == 0) + { + if (brief) + { + return Collections.singletonList(Messenger.c("b"+coloredName,"w : ","gi -, -/h, - min ")); + } + return Collections.singletonList(Messenger.c(coloredName, "w hasn't started counting yet")); + } + long total = getTotalItems(); + if (total == 0) + { + if (brief) + { + return Collections.singletonList(Messenger.c("b"+coloredName,"w : ","wb 0","w , ","wb 0","w /h, ", String.format("wb %.1f ", ticks / (20.0 * 60.0)), "w min")); + } + return Collections.singletonList(Messenger.c("w No items for ", coloredName, String.format("w yet (%.2f min.%s)", + ticks / (20.0 * 60.0), (realTime ? " - real time" : "")), + "nb [X]", "^g reset", "!/counter " + color.getName() +" reset")); + } + if (brief) + { + return Collections.singletonList(Messenger.c("b"+coloredName,"w : ", + "wb "+total,"w , ", + "wb "+(total * (20 * 60 * 60) / ticks),"w /h, ", + String.format("wb %.1f ", ticks / (20.0 * 60.0)), "w min" + )); + } + List items = new ArrayList<>(); + items.add(Messenger.c("w Items for ", coloredName, + "w (",String.format("wb %.2f", ticks*1.0/(20*60)), "w min"+(realTime?" - real time":"")+"), ", + "w total: ", "wb "+total, "w , (",String.format("wb %.1f",total*1.0*(20*60*60)/ticks),"w /h):", + "nb [X]", "^g reset", "!/counter "+color+" reset" + )); + items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(e -> + { + Item item = e.getKey(); + MutableComponent itemName = Component.translatable(item.getDescriptionId()); + Style itemStyle = itemName.getStyle(); + TextColor color = guessColor(item, server.registryAccess()); + itemName.setStyle((color != null) ? itemStyle.withColor(color) : itemStyle.withItalic(true)); + long count = e.getLongValue(); + return Messenger.c("g - ", itemName, + "g : ","wb "+count,"g , ", + String.format("wb %.1f", count * (20.0 * 60.0 * 60.0) / ticks), "w /h" + ); + }).collect(Collectors.toList())); + return items; + } + + /** + * Converts a colour to have a low brightness and uniform colour, so when it prints the items in different colours + * it's not too flashy and bright, but enough that it's not dull to look at. + */ + public static int appropriateColor(int color) + { + if (color == 0) return MapColor.SNOW.col; + int r = (color >> 16 & 255); + int g = (color >> 8 & 255); + int b = (color & 255); + if (r < 70) r = 70; + if (g < 70) g = 70; + if (b < 70) b = 70; + return (r << 16) + (g << 8) + b; + } + + /** + * Maps items that don't get a good block to reference for colour, or those that colour is wrong to a number of blocks, so we can get their colours easily with the + * {@link Block#defaultMapColor()} method as these items have those same colours. + */ + private static final Map DEFAULTS = Map.ofEntries( + entry(Items.DANDELION, Blocks.YELLOW_WOOL), + entry(Items.POPPY, Blocks.RED_WOOL), + entry(Items.BLUE_ORCHID, Blocks.LIGHT_BLUE_WOOL), + entry(Items.ALLIUM, Blocks.MAGENTA_WOOL), + entry(Items.AZURE_BLUET, Blocks.SNOW_BLOCK), + entry(Items.RED_TULIP, Blocks.RED_WOOL), + entry(Items.ORANGE_TULIP, Blocks.ORANGE_WOOL), + entry(Items.WHITE_TULIP, Blocks.SNOW_BLOCK), + entry(Items.PINK_TULIP, Blocks.PINK_WOOL), + entry(Items.OXEYE_DAISY, Blocks.SNOW_BLOCK), + entry(Items.CORNFLOWER, Blocks.BLUE_WOOL), + entry(Items.WITHER_ROSE, Blocks.BLACK_WOOL), + entry(Items.LILY_OF_THE_VALLEY, Blocks.WHITE_WOOL), + entry(Items.BROWN_MUSHROOM, Blocks.BROWN_MUSHROOM_BLOCK), + entry(Items.RED_MUSHROOM, Blocks.RED_MUSHROOM_BLOCK), + entry(Items.STICK, Blocks.OAK_PLANKS), + entry(Items.GOLD_INGOT, Blocks.GOLD_BLOCK), + entry(Items.IRON_INGOT, Blocks.IRON_BLOCK), + entry(Items.DIAMOND, Blocks.DIAMOND_BLOCK), + entry(Items.NETHERITE_INGOT, Blocks.NETHERITE_BLOCK), + entry(Items.SUNFLOWER, Blocks.YELLOW_WOOL), + entry(Items.LILAC, Blocks.MAGENTA_WOOL), + entry(Items.ROSE_BUSH, Blocks.RED_WOOL), + entry(Items.PEONY, Blocks.PINK_WOOL), + entry(Items.CARROT, Blocks.ORANGE_WOOL), + entry(Items.APPLE,Blocks.RED_WOOL), + entry(Items.WHEAT,Blocks.HAY_BLOCK), + entry(Items.PORKCHOP, Blocks.PINK_WOOL), + entry(Items.RABBIT,Blocks.PINK_WOOL), + entry(Items.CHICKEN,Blocks.WHITE_TERRACOTTA), + entry(Items.BEEF,Blocks.NETHERRACK), + entry(Items.ENCHANTED_GOLDEN_APPLE,Blocks.GOLD_BLOCK), + entry(Items.COD,Blocks.WHITE_TERRACOTTA), + entry(Items.SALMON,Blocks.ACACIA_PLANKS), + entry(Items.ROTTEN_FLESH,Blocks.BROWN_WOOL), + entry(Items.PUFFERFISH,Blocks.YELLOW_TERRACOTTA), + entry(Items.TROPICAL_FISH,Blocks.ORANGE_WOOL), + entry(Items.POTATO,Blocks.WHITE_TERRACOTTA), + entry(Items.MUTTON, Blocks.RED_WOOL), + entry(Items.BEETROOT,Blocks.NETHERRACK), + entry(Items.MELON_SLICE,Blocks.MELON), + entry(Items.POISONOUS_POTATO,Blocks.SLIME_BLOCK), + entry(Items.SPIDER_EYE,Blocks.NETHERRACK), + entry(Items.GUNPOWDER,Blocks.GRAY_WOOL), + entry(Items.TURTLE_SCUTE,Blocks.LIME_WOOL), + entry(Items.ARMADILLO_SCUTE,Blocks.ANCIENT_DEBRIS), + entry(Items.FEATHER,Blocks.WHITE_WOOL), + entry(Items.FLINT,Blocks.BLACK_WOOL), + entry(Items.LEATHER,Blocks.SPRUCE_PLANKS), + entry(Items.GLOWSTONE_DUST,Blocks.GLOWSTONE), + entry(Items.PAPER,Blocks.WHITE_WOOL), + entry(Items.BRICK,Blocks.BRICKS), + entry(Items.INK_SAC,Blocks.BLACK_WOOL), + entry(Items.SNOWBALL,Blocks.SNOW_BLOCK), + entry(Items.WATER_BUCKET,Blocks.WATER), + entry(Items.LAVA_BUCKET,Blocks.LAVA), + entry(Items.MILK_BUCKET,Blocks.WHITE_WOOL), + entry(Items.CLAY_BALL, Blocks.CLAY), + entry(Items.COCOA_BEANS,Blocks.COCOA), + entry(Items.BONE,Blocks.BONE_BLOCK), + entry(Items.COD_BUCKET,Blocks.BROWN_TERRACOTTA), + entry(Items.PUFFERFISH_BUCKET,Blocks.YELLOW_TERRACOTTA), + entry(Items.SALMON_BUCKET,Blocks.PINK_TERRACOTTA), + entry(Items.TROPICAL_FISH_BUCKET,Blocks.ORANGE_TERRACOTTA), + entry(Items.SUGAR,Blocks.WHITE_WOOL), + entry(Items.BLAZE_POWDER,Blocks.GOLD_BLOCK), + entry(Items.ENDER_PEARL,Blocks.WARPED_PLANKS), + entry(Items.NETHER_STAR,Blocks.DIAMOND_BLOCK), + entry(Items.PRISMARINE_CRYSTALS,Blocks.SEA_LANTERN), + entry(Items.PRISMARINE_SHARD,Blocks.PRISMARINE), + entry(Items.RABBIT_HIDE,Blocks.OAK_PLANKS), + entry(Items.CHORUS_FRUIT,Blocks.PURPUR_BLOCK), + entry(Items.SHULKER_SHELL,Blocks.SHULKER_BOX), + entry(Items.NAUTILUS_SHELL,Blocks.BONE_BLOCK), + entry(Items.HEART_OF_THE_SEA,Blocks.CONDUIT), + entry(Items.HONEYCOMB,Blocks.HONEYCOMB_BLOCK), + entry(Items.NAME_TAG,Blocks.BONE_BLOCK), + entry(Items.TOTEM_OF_UNDYING,Blocks.YELLOW_TERRACOTTA), + entry(Items.TRIDENT,Blocks.PRISMARINE), + entry(Items.GHAST_TEAR,Blocks.WHITE_WOOL), + entry(Items.PHANTOM_MEMBRANE,Blocks.BONE_BLOCK), + entry(Items.EGG,Blocks.BONE_BLOCK), + //entry(Items.,Blocks.), + entry(Items.COPPER_INGOT,Blocks.COPPER_BLOCK), + entry(Items.AMETHYST_SHARD, Blocks.AMETHYST_BLOCK)); + + /** + * Gets the colour to print an item in when printing its count in a hopper counter. + */ + public static TextColor fromItem(Item item, RegistryAccess registryAccess) + { + if (DEFAULTS.containsKey(item)) return TextColor.fromRgb(appropriateColor(DEFAULTS.get(item).defaultMapColor().col)); + if (item instanceof DyeItem dye) return TextColor.fromRgb(appropriateColor(dye.getDyeColor().getMapColor().col)); + Block block = null; + final Registry itemRegistry = registryAccess.registryOrThrow(Registries.ITEM); + final Registry blockRegistry = registryAccess.registryOrThrow(Registries.BLOCK); + ResourceLocation id = itemRegistry.getKey(item); + if (item instanceof BlockItem blockItem) + { + block = blockItem.getBlock(); + } + else if (blockRegistry.getOptional(id).isPresent()) + { + block = blockRegistry.get(id); + } + if (block != null) + { + if (block instanceof AbstractBannerBlock) return TextColor.fromRgb(appropriateColor(((AbstractBannerBlock) block).getColor().getMapColor().col)); + if (block instanceof BeaconBeamBlock) return TextColor.fromRgb(appropriateColor( ((BeaconBeamBlock) block).getColor().getMapColor().col)); + return TextColor.fromRgb(appropriateColor( block.defaultMapColor().col)); + } + return null; + } + + /** + * Guesses the item's colour from the item itself. It first calls {@link HopperCounter#fromItem} to see if it has a + * valid colour there, if not just makes a guess, and if that fails just returns null + */ + public static TextColor guessColor(Item item, RegistryAccess registryAccess) + { + TextColor direct = fromItem(item, registryAccess); + if (direct != null) return direct; + if (CarpetServer.minecraft_server == null) return WHITE; + + ResourceLocation id = registryAccess.registryOrThrow(Registries.ITEM).getKey(item); + for (RecipeType type: registryAccess.registryOrThrow(Registries.RECIPE_TYPE)) + { + for (Recipe r: ((RecipeManagerInterface) CarpetServer.minecraft_server.getRecipeManager()).getAllMatching(type, id, registryAccess)) + { + for (Ingredient ingredient: r.getIngredients()) + { + for (Collection stacks : ((IngredientInterface) (Object) ingredient).getRecipeStacks()) + { + for (ItemStack iStak : stacks) + { + TextColor cand = fromItem(iStak.getItem(), registryAccess); + if (cand != null) + return cand; + } + } + } + } + } + return null; + } + + /** + * Returns the hopper counter for the given color + */ + public static HopperCounter getCounter(DyeColor color) { + return COUNTERS.get(color); + } + + /** + * Returns the hopper counter from the colour name, if not null + */ + public static HopperCounter getCounter(String color) + { + try + { + DyeColor colorEnum = DyeColor.valueOf(color.toUpperCase(Locale.ROOT)); + return COUNTERS.get(colorEnum); + } + catch (IllegalArgumentException e) + { + return null; + } + } + + /** + * The total number of items in the counter + */ + public long getTotalItems() + { + return counter.isEmpty()?0:counter.values().longStream().sum(); + } +} diff --git a/src/main/java/carpet/helpers/OptimizedExplosion.java b/src/main/java/carpet/helpers/OptimizedExplosion.java new file mode 100644 index 0000000..63bc00b --- /dev/null +++ b/src/main/java/carpet/helpers/OptimizedExplosion.java @@ -0,0 +1,619 @@ +package carpet.helpers; +//Author: masa + +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import carpet.logging.logHelpers.ExplosionLogHelper; +import carpet.mixins.ExplosionAccessor; +import carpet.CarpetSettings; +import carpet.utils.Messenger; +import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; + +import static carpet.script.CarpetEventServer.Event.EXPLOSION_OUTCOME; + +public class OptimizedExplosion +{ + private static List entitylist; + private static Vec3 vec3dmem; + private static long tickmem; + // For disabling the explosion particles and sound + public static int explosionSound = 0; + + // masa's optimizations + private static Object2DoubleOpenHashMap> densityCache = new Object2DoubleOpenHashMap<>(); + private static MutablePair pairMutable = new MutablePair<>(); + private static Object2ObjectOpenHashMap stateCache = new Object2ObjectOpenHashMap<>(); + private static Object2ObjectOpenHashMap fluidCache = new Object2ObjectOpenHashMap<>(); + private static BlockPos.MutableBlockPos posMutable = new BlockPos.MutableBlockPos(0, 0, 0); + private static ObjectOpenHashSet affectedBlockPositionsSet = new ObjectOpenHashSet<>(); + private static boolean firstRay; + private static boolean rayCalcDone; + private static ArrayList chances = new ArrayList<>(); + private static BlockPos blastChanceLocation; + + // Creating entity list for scarpet event + private static List entityList = new ArrayList<>(); + + public static void doExplosionA(Explosion e, ExplosionLogHelper eLogger) { + ExplosionAccessor eAccess = (ExplosionAccessor) e; + + entityList.clear(); + boolean eventNeeded = EXPLOSION_OUTCOME.isNeeded() && !eAccess.getLevel().isClientSide(); + blastCalc(e); + + if (!CarpetSettings.explosionNoBlockDamage && eAccess.getDamageSource() != null) { + rayCalcDone = false; + firstRay = true; + getAffectedPositionsOnPlaneY(e, 0, 0, 15, 0, 15); // bottom + getAffectedPositionsOnPlaneY(e, 15, 0, 15, 0, 15); // top + getAffectedPositionsOnPlaneX(e, 0, 1, 14, 0, 15); // west + getAffectedPositionsOnPlaneX(e, 15, 1, 14, 0, 15); // east + getAffectedPositionsOnPlaneZ(e, 0, 1, 14, 1, 14); // north + getAffectedPositionsOnPlaneZ(e, 15, 1, 14, 1, 14); // south + stateCache.clear(); + fluidCache.clear(); + + e.getToBlow().addAll(affectedBlockPositionsSet); + affectedBlockPositionsSet.clear(); + } + + float f3 = eAccess.getRadius() * 2.0F; + int k1 = Mth.floor(eAccess.getX() - (double) f3 - 1.0D); + int l1 = Mth.floor(eAccess.getX() + (double) f3 + 1.0D); + int i2 = Mth.floor(eAccess.getY() - (double) f3 - 1.0D); + int i1 = Mth.floor(eAccess.getY() + (double) f3 + 1.0D); + int j2 = Mth.floor(eAccess.getZ() - (double) f3 - 1.0D); + int j1 = Mth.floor(eAccess.getZ() + (double) f3 + 1.0D); + Vec3 vec3d = new Vec3(eAccess.getX(), eAccess.getY(), eAccess.getZ()); + + if (vec3dmem == null || !vec3dmem.equals(vec3d) || tickmem != eAccess.getLevel().getGameTime()) { + vec3dmem = vec3d; + tickmem = eAccess.getLevel().getGameTime(); + entitylist = eAccess.getLevel().getEntities(null, new AABB(k1, i2, j2, l1, i1, j1)); + explosionSound = 0; + } + + explosionSound++; + + Entity explodingEntity = eAccess.getSource(); + for (int k2 = 0; k2 < entitylist.size(); ++k2) { + Entity entity = entitylist.get(k2); + + + if (entity == explodingEntity) { + // entitylist.remove(k2); + removeFast(entitylist, k2); + k2--; + continue; + } + + if (entity instanceof PrimedTnt && explodingEntity != null && + entity.getX() == explodingEntity.getX() && + entity.getY() == explodingEntity.getY() && + entity.getZ() == explodingEntity.getZ()) { + if (eLogger != null) { + eLogger.onEntityImpacted(entity, new Vec3(0,-0.9923437498509884d, 0)); + } + continue; + } + + if (!entity.ignoreExplosion(e)) { + double d12 = Math.sqrt(entity.distanceToSqr(eAccess.getX(), eAccess.getY(), eAccess.getZ())) / (double) f3; + + if (d12 <= 1.0D) { + double d5 = entity.getX() - eAccess.getX(); + // Change in 1.16 snapshots to fix a bug with TNT jumping + double d7 = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - eAccess.getY(); + double d9 = entity.getZ() - eAccess.getZ(); + double d13 = (double) Math.sqrt(d5 * d5 + d7 * d7 + d9 * d9); + + if (d13 != 0.0D) { + d5 = d5 / d13; + d7 = d7 / d13; + d9 = d9 / d13; + double density; + + pairMutable.setLeft(vec3d); + pairMutable.setRight(entity.getBoundingBox()); + density = densityCache.getOrDefault(pairMutable, Double.MAX_VALUE); + + if (density == Double.MAX_VALUE) + { + Pair pair = Pair.of(vec3d, entity.getBoundingBox()); + density = Explosion.getSeenPercent(vec3d, entity); + densityCache.put(pair, density); + } + + // If it is needed, it saves the entity + if (eventNeeded) { + entityList.add(entity); + } + + double d10 = (1.0D - d12) * density; + if (eAccess.getDamageSource() != null) + { + entity.hurt(eAccess.getDamageSource(), + (float) ((int) ((d10 * d10 + d10) / 2.0D * 7.0D * (double) f3 + 1.0D))); + } + double d11 = d10; + + if (entity instanceof LivingEntity lev) { + d11 = d10 * Mth.clamp(1.0 - lev.getAttributeValue(Attributes.EXPLOSION_KNOCKBACK_RESISTANCE), 0.0, 1.0); + } + + if (eLogger != null) { + eLogger.onEntityImpacted(entity, new Vec3(d5 * d11, d7 * d11, d9 * d11)); + } + + entity.setDeltaMovement(entity.getDeltaMovement().add(d5 * d11, d7 * d11, d9 * d11)); + + if (entity instanceof Player player) { + + if (!player.isSpectator() + && (!player.isCreative() || !player.getAbilities().flying)) { //getAbilities + e.getHitPlayers().put(player, new Vec3(d5 * d10, d7 * d10, d9 * d10)); + } + } + } + } + } + } + + densityCache.clear(); + } + + public static void doExplosionB(Explosion e, boolean spawnParticles) + { + ExplosionAccessor eAccess = (ExplosionAccessor) e; + Level world = eAccess.getLevel(); + double posX = eAccess.getX(); + double posY = eAccess.getY(); + double posZ = eAccess.getZ(); + + // If it is needed, calls scarpet event + if (EXPLOSION_OUTCOME.isNeeded() && !world.isClientSide()) { + EXPLOSION_OUTCOME.onExplosion((ServerLevel) world, eAccess.getSource(), e::getIndirectSourceEntity, eAccess.getX(), eAccess.getY(), eAccess.getZ(), eAccess.getRadius(), eAccess.isFire(), e.getToBlow(), entityList, eAccess.getBlockInteraction()); + } + + boolean damagesTerrain = eAccess.getBlockInteraction() != Explosion.BlockInteraction.KEEP; + + // explosionSound incremented till disabling the explosion particles and sound + if (explosionSound < 100 || explosionSound % 100 == 0) + { + world.playSound(null, posX, posY, posZ, SoundEvents.GENERIC_EXPLODE.value(), SoundSource.BLOCKS, 4.0F, + (1.0F + (world.random.nextFloat() - world.random.nextFloat()) * 0.2F) * 0.7F); + + if (spawnParticles) + { + if (eAccess.getRadius() >= 2.0F && damagesTerrain) + { + world.addParticle(ParticleTypes.EXPLOSION_EMITTER, posX, posY, posZ, 1.0D, 0.0D, 0.0D); + } + else + { + world.addParticle(ParticleTypes.EXPLOSION, posX, posY, posZ, 1.0D, 0.0D, 0.0D); + } + } + } + + if (damagesTerrain) + { + ObjectArrayList> objectArrayList = new ObjectArrayList<>(); + Util.shuffle((ObjectArrayList) e.getToBlow(), world.random); + + boolean dropFromExplosions = CarpetSettings.xpFromExplosions || e.getIndirectSourceEntity() instanceof Player; + + for (BlockPos blockpos : e.getToBlow()) + { + BlockState state = world.getBlockState(blockpos); + Block block = state.getBlock(); + + if (!state.isAir()) + { + if (block.dropFromExplosion(e) && world instanceof ServerLevel serverLevel) + { + BlockEntity blockEntity = state.hasBlockEntity() ? world.getBlockEntity(blockpos) : null; //hasBlockEntity() + + LootParams.Builder lootBuilder = (new LootParams.Builder((ServerLevel)eAccess.getLevel())) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(blockpos)) + .withParameter(LootContextParams.TOOL, ItemStack.EMPTY) + .withOptionalParameter(LootContextParams.BLOCK_ENTITY, blockEntity) + .withOptionalParameter(LootContextParams.THIS_ENTITY, eAccess.getSource()); + + if (eAccess.getBlockInteraction() == Explosion.BlockInteraction.DESTROY_WITH_DECAY) + lootBuilder.withParameter(LootContextParams.EXPLOSION_RADIUS, eAccess.getRadius()); + + state.spawnAfterBreak(serverLevel, blockpos, ItemStack.EMPTY, dropFromExplosions); + + state.getDrops(lootBuilder).forEach((itemStackx) -> { + method_24023(objectArrayList, itemStackx, blockpos.immutable()); + }); + } + + world.setBlock(blockpos, Blocks.AIR.defaultBlockState(), 3); + block.wasExploded(world, blockpos, e); + } + } + objectArrayList.forEach(p -> Block.popResource(world, p.getRight(), p.getLeft())); + + } + + if (eAccess.isFire()) + { + for (BlockPos blockpos1 : e.getToBlow()) + { + // Use the same Chunk reference because the positions are in the same xz-column + ChunkAccess chunk = world.getChunk(blockpos1.getX() >> 4, blockpos1.getZ() >> 4); + + BlockPos down = blockpos1.below(1); + if (eAccess.getRandom().nextInt(3) == 0 && + chunk.getBlockState(blockpos1).isAir() && + chunk.getBlockState(down).isSolidRender(world, down) + ) + { + world.setBlockAndUpdate(blockpos1, Blocks.FIRE.defaultBlockState()); + } + } + } + } + + // copied from Explosion, need to move the code to the explosion code anyways and use shadows for + // simplicity, its not jarmodding anyways + private static void method_24023(ObjectArrayList> objectArrayList, ItemStack itemStack, BlockPos blockPos) { + int i = objectArrayList.size(); + + for(int j = 0; j < i; ++j) { + Pair pair = objectArrayList.get(j); + ItemStack itemStack2 = pair.getLeft(); + if (ItemEntity.areMergable(itemStack2, itemStack)) { + ItemStack itemStack3 = ItemEntity.merge(itemStack2, itemStack, 16); + objectArrayList.set(j, Pair.of(itemStack3, pair.getRight())); + if (itemStack.isEmpty()) { + return; + } + } + } + + objectArrayList.add(Pair.of(itemStack, blockPos)); + } + + private static void removeFast(List lst, int index) { + if (index < lst.size() - 1) + lst.set(index, lst.get(lst.size() - 1)); + lst.remove(lst.size() - 1); + } + + private static void rayCalcs(Explosion e) { + ExplosionAccessor eAccess = (ExplosionAccessor) e; + boolean first = true; + + for (int j = 0; j < 16; ++j) { + for (int k = 0; k < 16; ++k) { + for (int l = 0; l < 16; ++l) { + if (j == 0 || j == 15 || k == 0 || k == 15 || l == 0 || l == 15) { + double d0 = (double) ((float) j / 15.0F * 2.0F - 1.0F); + double d1 = (double) ((float) k / 15.0F * 2.0F - 1.0F); + double d2 = (double) ((float) l / 15.0F * 2.0F - 1.0F); + double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); + d0 = d0 / d3; + d1 = d1 / d3; + d2 = d2 / d3; + float rand = eAccess.getLevel().random.nextFloat(); + if (CarpetSettings.tntRandomRange >= 0) { + rand = (float) CarpetSettings.tntRandomRange; + } + float f = eAccess.getRadius() * (0.7F + rand * 0.6F); + double d4 = eAccess.getX(); + double d6 = eAccess.getY(); + double d8 = eAccess.getZ(); + + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { + BlockPos blockpos = BlockPos.containing(d4, d6, d8); + BlockState state = eAccess.getLevel().getBlockState(blockpos); + FluidState fluidState = eAccess.getLevel().getFluidState(blockpos); + + if (!state.isAir()) { + float f2 = Math.max(state.getBlock().getExplosionResistance(), fluidState.getExplosionResistance()); + if (eAccess.getSource() != null) + f2 = eAccess.getSource().getBlockExplosionResistance(e, eAccess.getLevel(), blockpos, state, fluidState, f2); + f -= (f2 + 0.3F) * 0.3F; + } + + if (f > 0.0F && (eAccess.getSource() == null || + eAccess.getSource().shouldBlockExplode(e, eAccess.getLevel(), blockpos, state, f))) + { + affectedBlockPositionsSet.add(blockpos); + } + else if (first) { + return; + } + + first = false; + + d4 += d0 * 0.30000001192092896D; + d6 += d1 * 0.30000001192092896D; + d8 += d2 * 0.30000001192092896D; + } + } + } + } + } + } + + private static void getAffectedPositionsOnPlaneX(Explosion e, int x, int yStart, int yEnd, int zStart, int zEnd) + { + if (!rayCalcDone) + { + final double xRel = (double) x / 15.0D * 2.0D - 1.0D; + + for (int z = zStart; z <= zEnd; ++z) + { + double zRel = (double) z / 15.0D * 2.0D - 1.0D; + + for (int y = yStart; y <= yEnd; ++y) + { + double yRel = (double) y / 15.0D * 2.0D - 1.0D; + + if (checkAffectedPosition(e, xRel, yRel, zRel)) + { + return; + } + } + } + } + } + + private static void getAffectedPositionsOnPlaneY(Explosion e, int y, int xStart, int xEnd, int zStart, int zEnd) + { + if (!rayCalcDone) + { + final double yRel = (double) y / 15.0D * 2.0D - 1.0D; + + for (int z = zStart; z <= zEnd; ++z) + { + double zRel = (double) z / 15.0D * 2.0D - 1.0D; + + for (int x = xStart; x <= xEnd; ++x) + { + double xRel = (double) x / 15.0D * 2.0D - 1.0D; + + if (checkAffectedPosition(e, xRel, yRel, zRel)) + { + return; + } + } + } + } + } + + private static void getAffectedPositionsOnPlaneZ(Explosion e, int z, int xStart, int xEnd, int yStart, int yEnd) + { + if (!rayCalcDone) + { + final double zRel = (double) z / 15.0D * 2.0D - 1.0D; + + for (int x = xStart; x <= xEnd; ++x) + { + double xRel = (double) x / 15.0D * 2.0D - 1.0D; + + for (int y = yStart; y <= yEnd; ++y) + { + double yRel = (double) y / 15.0D * 2.0D - 1.0D; + + if (checkAffectedPosition(e, xRel, yRel, zRel)) + { + return; + } + } + } + } + } + + private static boolean checkAffectedPosition(Explosion e, double xRel, double yRel, double zRel) + { + ExplosionAccessor eAccess = (ExplosionAccessor) e; + double len = Math.sqrt(xRel * xRel + yRel * yRel + zRel * zRel); + double xInc = (xRel / len) * 0.3; + double yInc = (yRel / len) * 0.3; + double zInc = (zRel / len) * 0.3; + float rand = eAccess.getLevel().random.nextFloat(); + float sizeRand = (CarpetSettings.tntRandomRange >= 0 ? (float) CarpetSettings.tntRandomRange : rand); + float size = eAccess.getRadius() * (0.7F + sizeRand * 0.6F); + double posX = eAccess.getX(); + double posY = eAccess.getY(); + double posZ = eAccess.getZ(); + + for (float f1 = 0.3F; size > 0.0F; size -= 0.22500001F) + { + posMutable.set(posX, posY, posZ); + + // Don't query already cached positions again from the world + BlockState state = stateCache.get(posMutable); + FluidState fluid = fluidCache.get(posMutable); + BlockPos posImmutable = null; + + if (state == null) + { + posImmutable = posMutable.immutable(); + state = eAccess.getLevel().getBlockState(posImmutable); + stateCache.put(posImmutable, state); + fluid = eAccess.getLevel().getFluidState(posImmutable); + fluidCache.put(posImmutable, fluid); + } + + if (!state.isAir()) + { + float resistance = Math.max(state.getBlock().getExplosionResistance(), fluid.getExplosionResistance()); + + if (eAccess.getSource() != null) + { + resistance = eAccess.getSource().getBlockExplosionResistance(e, eAccess.getLevel(), posMutable, state, fluid, resistance); + } + + size -= (resistance + 0.3F) * 0.3F; + } + + if (size > 0.0F) + { + if ((eAccess.getSource() == null || eAccess.getSource().shouldBlockExplode(e, eAccess.getLevel(), posMutable, state, size))) + affectedBlockPositionsSet.add(posImmutable != null ? posImmutable : posMutable.immutable()); + } + else if (firstRay) + { + rayCalcDone = true; + return true; + } + + firstRay = false; + + posX += xInc; + posY += yInc; + posZ += zInc; + } + + return false; + } + + public static void setBlastChanceLocation(BlockPos p){ + blastChanceLocation = p; + } + + private static void blastCalc(Explosion e){ + ExplosionAccessor eAccess = (ExplosionAccessor) e; + if(blastChanceLocation == null || blastChanceLocation.distToLowCornerSqr(eAccess.getX(), eAccess.getY(), eAccess.getZ()) > 200) return; + chances.clear(); + for (int j = 0; j < 16; ++j) { + for (int k = 0; k < 16; ++k) { + for (int l = 0; l < 16; ++l) { + if (j == 0 || j == 15 || k == 0 || k == 15 || l == 0 || l == 15) { + double d0 = (double) ((float) j / 15.0F * 2.0F - 1.0F); + double d1 = (double) ((float) k / 15.0F * 2.0F - 1.0F); + double d2 = (double) ((float) l / 15.0F * 2.0F - 1.0F); + double d3 = Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2); + d0 = d0 / d3; + d1 = d1 / d3; + d2 = d2 / d3; + float f = eAccess.getRadius() * (0.7F + 0.6F); + double d4 = eAccess.getX(); + double d6 = eAccess.getY(); + double d8 = eAccess.getZ(); + boolean found = false; + + for (float f1 = 0.3F; f > 0.0F; f -= 0.22500001F) { + BlockPos blockpos = BlockPos.containing(d4, d6, d8); + BlockState state = eAccess.getLevel().getBlockState(blockpos); + FluidState fluidState = eAccess.getLevel().getFluidState(blockpos); + + if (!state.isAir()) { + float f2 = Math.max(state.getBlock().getExplosionResistance(), fluidState.getExplosionResistance()); + if (eAccess.getSource() != null) + f2 = eAccess.getSource().getBlockExplosionResistance(e, eAccess.getLevel(), blockpos, state, fluidState, f2); + f -= (f2 + 0.3F) * 0.3F; + } + + if (f > 0.0F && (eAccess.getSource() == null || + eAccess.getSource().shouldBlockExplode(e, eAccess.getLevel(), blockpos, state, f))) { + if(!found && blockpos.equals(blastChanceLocation)){ + chances.add(f); + found = true; + } + } + + d4 += d0 * 0.30000001192092896D; + d6 += d1 * 0.30000001192092896D; + d8 += d2 * 0.30000001192092896D; + } + } + } + } + } + + //showTNTblastChance(e); + } + + private static void showTNTblastChance(Explosion e){ + ExplosionAccessor eAccess = (ExplosionAccessor) e; + double randMax = 0.6F * eAccess.getRadius(); + double total = 0; + boolean fullyBlownUp = false; + boolean first = true; + int rays = 0; + for(float f3 : chances){ + rays++; + double calc = f3 - randMax; + if(calc > 0) fullyBlownUp = true; + double chancePerRay = (Math.abs(calc) / randMax); + if(!fullyBlownUp){ + if(first){ + first = false; + total = chancePerRay; + }else { + total = total * chancePerRay; + } + } + } + if(fullyBlownUp) total = 0; + double chance = 1 - total; + NumberFormat nf = NumberFormat.getNumberInstance(); + nf.setRoundingMode (RoundingMode.DOWN); + nf.setMaximumFractionDigits(2); + for(Player player : eAccess.getLevel().players()){ + Messenger.m(player,"w Pop: ", + "c " + nf.format(chance) + " ", + "^w Chance for the block to be destroyed by the blast: " + chance, + "?" + chance, + "w Remain: ", + String.format("c %.2f ", total), + "^w Chance the block survives the blast: " + total, + "?" + total, + "w Rays: ", + String.format("c %d ", rays), + "^w TNT blast rays going through the block", + "?" + rays, + "w Size: ", + String.format("c %.1f ", eAccess.getRadius()), + "^w TNT blast size", + "?" + eAccess.getRadius(), + "w @: ", + String.format("c [%.1f %.1f %.1f] ", eAccess.getX(), eAccess.getY(), eAccess.getZ()), + "^w TNT blast location X:" + eAccess.getX() + " Y:" + eAccess.getY() + " Z:" + eAccess.getZ(), + "?" + eAccess.getX() + " " + eAccess.getY() + " " + eAccess.getZ() + ); + } + } +} diff --git a/src/main/java/carpet/helpers/ParticleDisplay.java b/src/main/java/carpet/helpers/ParticleDisplay.java new file mode 100644 index 0000000..0184375 --- /dev/null +++ b/src/main/java/carpet/helpers/ParticleDisplay.java @@ -0,0 +1,42 @@ +package carpet.helpers; + +import carpet.script.utils.ParticleParser; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleType; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; + +public class ParticleDisplay +{ + public static void drawParticleLine(ServerPlayer player, Vec3 from, Vec3 to, String main, String accent, int count, double spread) + { + ParticleOptions accentParticle = ParticleParser.getEffect(accent, player.server.registryAccess()); + ParticleOptions mainParticle = ParticleParser.getEffect(main, player.server.registryAccess()); + + if (accentParticle != null) player.serverLevel().sendParticles( + player, + accentParticle, + true, + to.x, to.y, to.z, count, + spread, spread, spread, 0.0); + + double lineLengthSq = from.distanceToSqr(to); + if (lineLengthSq == 0) return; + + Vec3 incvec = to.subtract(from).normalize();// multiply(50/sqrt(lineLengthSq)); + for (Vec3 delta = new Vec3(0.0,0.0,0.0); + delta.lengthSqr() < lineLengthSq; + delta = delta.add(incvec.scale(player.level().random.nextFloat()))) + { + player.serverLevel().sendParticles( + player, + mainParticle, + true, + delta.x+from.x, delta.y+from.y, delta.z+from.z, 1, + 0.0, 0.0, 0.0, 0.0); + } + } + +} diff --git a/src/main/java/carpet/helpers/QuasiConnectivity.java b/src/main/java/carpet/helpers/QuasiConnectivity.java new file mode 100644 index 0000000..5bc411a --- /dev/null +++ b/src/main/java/carpet/helpers/QuasiConnectivity.java @@ -0,0 +1,24 @@ +package carpet.helpers; + +import carpet.CarpetSettings; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.SignalGetter; + +public class QuasiConnectivity { + + public static boolean hasQuasiSignal(SignalGetter level, BlockPos pos) { + for (int i = 1; i <= CarpetSettings.quasiConnectivity; i++) { + BlockPos above = pos.above(i); + + if (level.isOutsideBuildHeight(above)) { + break; + } + if (level.hasNeighborSignal(above)) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/carpet/helpers/RedstoneWireTurbo.java b/src/main/java/carpet/helpers/RedstoneWireTurbo.java new file mode 100644 index 0000000..30a1c37 --- /dev/null +++ b/src/main/java/carpet/helpers/RedstoneWireTurbo.java @@ -0,0 +1,968 @@ +package carpet.helpers; +//Author: theosib + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.ThreadLocalRandom; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RedStoneWireBlock; +import net.minecraft.world.level.block.state.BlockState; +import carpet.fakes.RedstoneWireBlockInterface; + +public class RedstoneWireTurbo +{ + /* + * This is Helper class for RedstoneWireBlock. It implements a minimially-invasive + * bolt-on accelerator that performs a breadth-first search through redstone wire blocks + * in order to more efficiently and deterministicly compute new redstone wire power levels + * and determine the order in which other blocks should be updated. + * + * Features: + * - Changes to RedstoneWireBlock are very limited, no other classes are affected, and the + * choice between old and new redstone wire update algorithms is switchable on-line. + * - The vanilla implementation relied on World.notifyNeighborsOfStateChange for redstone + * wire blocks to communicate power level changes to each other, generating 36 block + * updates per call. This improved implementation propagates power level changes directly + * between redstone wire blocks. Redstone wire power levels are therefore computed more quickly, + * and block updates are sent only to non-redstone blocks, many of which may perform an + * action when informed of a change in redstone power level. (Note: Block updates are not + * the same as state changes to redstone wire. Wire block states are updated as soon + * as they are computed.) + * - Of the 36 block updates generated by a call to World.notifyNeighborsOfStateChange, + * 12 of them are obviously redundant (e.g. the west neighbor of the east neighbor). + * These are eliminated. + * - Updates to redstone wire and other connected blocks are propagated in a breath-first + * manner, radiating out from the initial trigger (a block update to a redstone wire + * from something other than redstone wire). + * - Updates are scheduled both deterministicly and in an intuitive order, addressing bug + * MC-11193. + * - All redstone behavior that used to be locational now works the same in all locations. + * - All behaviors of redstone wire that used to be orientational now work the same in all + * orientations, as long as orientation can be determined; random otherwise. Some other + * redstone components still update directionally (e.g. switches), and this code can't + * compensate for that. + * - Information that is otherwise computed over and over again or which is expensive to + * to compute is cached for faster lookup. This includes coordinates of block position + * neighbors and block states that won't change behind our backs during the execution of + * this search algorithm. + * - Redundant block updates (both to redstone wire and to other blocks) are heavily + * consolidated. For worst-case scenarios (depowering of redstone wire) this results + * in a reduction of block updates by as much as 95% (factor of 1/21). Due to overheads, + * empirical testing shows a speedup better than 10x. This addresses bug MC-81098. + * + * Extensive testing has been performed to ensure that existing redstone contraptions still + * behave as expected. Results of early testing that identified undesirable behavior changes + * were addressed. Additionally, real-time performance testing revealed compute inefficiencies + * With earlier implementations of this accelerator. Some compatibility adjustments and + * performance optimizations resulted in harmless increases in block updates above the + * theoretical minimum. + * + * Only a single redstone machine was found to break: An instant dropper line hack that + * relies on powered rails and quasiconnectivity but doesn't work in all directions. The + * replacement is to lay redstone wire directly on top of the dropper line, which now works + * reliably in any direction. + * + * There are numerous other optimization that can be made, but those will be provided later in + * separate updates. This version is designed to be minimalistic. + * + * Many thanks to the following individuals for their help in testing this functionality: + * - pokechu22, _MethodZz_, WARBEN, NarcolepticFrog, CommandHelper (nessie), ilmango, + * OreoLamp, Xcom6000, tryashtar, RedCMD, Smokey95Dog, EDDxample, Rays Works, + * Nodnam, BlockyPlays, Grumm, NeunEinser, HelVince. + */ + + + /* Reference to RedstoneWireBlock object, which uses this accelerator */ + private final RedStoneWireBlock wire; + + + /* + * Implementation: + * + * RedstoneWire Blocks are updated in concentric rings or "layers" radiating out from the + * initial block update that came from a call to RedstoneWireBlock.neighborChanged(). + * All nodes put in Layer N are those with Manattan distance N from the trigger + * position, reachable through connected redstone wire blocks. + * + * Layer 0 represents the trigger block position that was input to neighborChanged. + * Layer 1 contains the immediate neighbors of that position. + * Layer N contains the neighbors of blocks in layer N-1, not including + * those in previous layers. + * + * Layers enforce an update order that is a function of Manhattan distance + * from the initial coordinates input to neighborChanged. The same + * coordinates may appear in multiple layers, but redundant updates are minimized. + * Block updates are sent layer-by-layer. If multiple of a block's neighbors experience + * redstone wire changes before its layer is processed, then those updates will be merged. + * If a block's update has been sent, but its neighboring redstone changes + * after that, then another update will be sent. This preserves compatibility with + * machines that rely on zero-tick behavior, except that the new functionality is non- + * locational. + * + * Within each layer, updates are ordered left-to-right relative to the direction of + * information flow. This makes the implementation non-orientational. Only when + * this direction is ambiguous is randomness applied (intentionally). + */ + //private final List> updateLayers = new ArrayList<>(); + private List updateQueue0 = new ArrayList<>(); + private List updateQueue1 = new ArrayList<>(); + private List updateQueue2 = new ArrayList<>(); + + + public RedstoneWireTurbo(RedStoneWireBlock wire) { + this.wire = wire; + } + + + /* + * Compute neighbors of a block. When a redstone wire value changes, previously it called + * World.notifyNeighborsOfStateChange. That lists immediately neighboring blocks in + * west, east, down, up, north, south order. For each of those neighbors, their own + * neighbors are updated in the same order. This generates 36 updates, but 12 of them are + * redundant; for instance the west neighbor of a block's east neighbor. + * + * Note that this ordering is only used to create the initial list of neighbors. Once + * the direction of signal flow is identified, the ordering of updates is completely + * reorganized. + */ + public static BlockPos[] computeAllNeighbors(final BlockPos pos) { + final int x = pos.getX(); + final int y = pos.getY(); + final int z = pos.getZ(); + final BlockPos[] n = new BlockPos[24]; + + // Immediate neighbors, in the same order as + // World.notifyNeighborsOfStateChange, etc.: + // west, east, down, up, north, south + n[ 0] = new BlockPos(x-1, y , z ); + n[ 1] = new BlockPos(x+1, y , z ); + n[ 2] = new BlockPos(x , y-1, z ); + n[ 3] = new BlockPos(x , y+1, z ); + n[ 4] = new BlockPos(x , y , z-1); + n[ 5] = new BlockPos(x , y , z+1); + + // Neighbors of neighbors, in the same order, + // except that duplicates are not included + n[ 6] = new BlockPos(x-2, y , z ); + n[ 7] = new BlockPos(x-1, y-1, z ); + n[ 8] = new BlockPos(x-1, y+1, z ); + n[ 9] = new BlockPos(x-1, y , z-1); + n[10] = new BlockPos(x-1, y , z+1); + n[11] = new BlockPos(x+2, y , z ); + n[12] = new BlockPos(x+1, y-1, z ); + n[13] = new BlockPos(x+1, y+1, z ); + n[14] = new BlockPos(x+1, y , z-1); + n[15] = new BlockPos(x+1, y , z+1); + n[16] = new BlockPos(x , y-2, z ); + n[17] = new BlockPos(x , y-1, z-1); + n[18] = new BlockPos(x , y-1, z+1); + n[19] = new BlockPos(x , y+2, z ); + n[20] = new BlockPos(x , y+1, z-1); + n[21] = new BlockPos(x , y+1, z+1); + n[22] = new BlockPos(x , y , z-2); + n[23] = new BlockPos(x , y , z+2); + return n; + } + + /* + * We only want redstone wires to update redstone wires that are + * immediately adjacent. Some more distant updates can result + * in cross-talk that (a) wastes time and (b) can make the update + * order unintuitive. Therefore (relative to the neighbor order + * computed by computeAllNeighbors), updates are not scheduled + * for redstone wire in those non-connecting positions. On the + * other hand, updates will always be sent to *other* types of blocks + * in any of the 24 neighboring positions. + */ + private static final boolean[] update_redstone = { + true, true, false, false, true, true, // 0 to 5 + false, true, true, false, false, false, // 6 to 11 + true, true, false, false, false, true, // 12 to 17 + true, false, true, true, false, false}; // 18 to 23 + + // Internal numbering for cardinal directions + private static final int North = 0; + private static final int East = 1; + private static final int South = 2; + private static final int West = 3; + + // Names for debug print statements + private static final char[] dirname = {'N', 'E', 'S', 'W'}; + + /* + * These lookup tables completely remap neighbor positions into a left-to-right + * ordering, based on the cardinal direction that is determined to be forward. + * See below for more explanation. + */ + private static final int[] forward_is_north = {2, 3, 16, 19, 0, 4, 1, 5, 7, 8, 17, 20, 12, 13, 18, 21, 6, 9, 22, 14, 11, 10, 23, 15}; + private static final int[] forward_is_east = {2, 3, 16, 19, 4, 1, 5, 0, 17, 20, 12, 13, 18, 21, 7, 8, 22, 14, 11, 15, 23, 9, 6, 10}; + private static final int[] forward_is_south = {2, 3, 16, 19, 1, 5, 0, 4, 12, 13, 18, 21, 7, 8, 17, 20, 11, 15, 23, 10, 6, 14, 22, 9}; + private static final int[] forward_is_west = {2, 3, 16, 19, 5, 0, 4, 1, 18, 21, 7, 8, 17, 20, 12, 13, 23, 10, 6, 9, 22, 15, 11, 14}; + + /* For any orientation, we end up with the update order defined below. This order is relative to any redstone wire block + * that is itself having an update computed, and this center position is marked with C. + * - The update position marked 0 is computed first, and the one marked 23 is last. + * - Forward is determined by the local direction of information flow into position C from prior updates. + * - The first updates are scheduled for the four positions below and above C. + * - Then updates are scheduled for the four horizontal neighbors of C, followed by the positions below and above those neighbors. + * - Finally, updates are scheduled for the remaining positions with Manhattan distance 2 from C (at the same Y coordinate). + * - For a given horizontal distance from C, updates are scheduled starting from directly left and stepping clockwise to directly + * right. The remaining positions behind C are scheduled counterclockwise so as to maintain the left-to-right ordering. + * - If C is in layer N of the update schedule, then all 24 positions may be scheduled for layer N+1. For redstone wire, no + * updates are scheduled for positions that cannot directly connect. Additionally, the four positions above and below C + * are ALSO scheduled for layer N+2. + * - This update order was selected after experimenting with a number of alternative schedules, based on its compatibility + * with existing redstone designs and behaviors that were considered to be intuitive by various testers. WARBEN in particular + * made some of the most challenging test cases, but the 3-tick clocks (made by RedCMD) were also challenging to fix, + * along with the rail-based instant dropper line built by ilmango. Numerous others made test cases as well, including + * NarcolepticFrog, nessie, and Pokechu22. + * + * - The forward direction ls determined locally. So when there are branches in the redstone wire, the left one will get updated + * before the right one. Each branch can have its own relative forward direction, resulting in the left side of a left branch + * having priority over the right branch of a left branch, which has priority over the left branch of a right branch, followed + * by the right branch of a right branch. And so forth. Since redstone power reduces to zero after a path distance of 15, + * that imposes a practical limit on the branching. Note that the branching is not tracked explicitly -- relative forward + * directions dictate relative sort order, which maintains the proper global ordering. This also makes it unnecessary to be + * concerned about branches meeting up with each other. + * + * ^ + * | + * Forward + * <-- Left Right --> + * + * 18 + * 10 17 5 19 11 + * 2 8 0 12 16 4 C 6 20 9 1 13 3 + * 14 21 7 23 15 + * Further 22 Further + * Down Down Up Up + * + * Backward + * | + * V + */ + + // This allows the above remapping tables to be looked up by cardial direction index + private static final int[][] reordering = {forward_is_north, forward_is_east, forward_is_south, forward_is_west}; + + /* + * Input: Array of UpdateNode objects in an order corresponding to the positions + * computed by computeAllNeighbors above. + * Output: Array of UpdateNode objects oriented using the above remapping tables + * corresponding to the identified heading (direction of information flow). + */ + private static void orientNeighbors(final UpdateNode[] src, final UpdateNode[] dst, final int heading) { + final int[] re = reordering[heading]; + for (int i=0; i<24; i++) { + dst[i] = src[re[i]]; + } + } + + /* + * Structure to keep track of redstone wire blocks and + * neighbors that will receive updates. + */ + private static class UpdateNode { + public enum Type { + UNKNOWN, REDSTONE, OTHER + } + + BlockState currentState; // Keep track of redstone wire value + UpdateNode[] neighbor_nodes; // References to neighbors (directed graph edges) + BlockPos self; // UpdateNode's own position + BlockPos parent; // Which block pos spawned/updated this node + Type type = Type.UNKNOWN; // unknown, redstone wire, other type of block + int layer; // Highest layer this node is scheduled in + boolean visited; // To keep track of information flow direction, visited restone wire is marked + int xbias, zbias; // Remembers directionality of ancestor nodes; helps eliminate directional ambiguities. + } + + + /* + * Keep track of all block positions discovered during search and their current states. + * We want to remember one entry for each position. + */ + private final Map nodeCache = new HashMap<>(); + + + /* + * For a newly created UpdateNode object, determine what type of block it is. + */ + private void identifyNode(final Level worldIn, final UpdateNode upd1) { + final BlockPos pos = upd1.self; + final BlockState oldState = worldIn.getBlockState(pos); + upd1.currentState = oldState; + + // Some neighbors of redstone wire are other kinds of blocks. + // These need to receive block updates to inform them that + // redstone wire values have changed. + final Block block = oldState.getBlock(); + if (block != wire) { + // Mark this block as not redstone wire and therefore + // requiring updates + upd1.type = UpdateNode.Type.OTHER; + + // Non-redstone blocks may propagate updates, but those updates + // are not handled by this accelerator. Therefore, we do not + // expand this position's neighbors. + return; + } + + // One job of RedstoneWireBlock.neighborChanged is to convert + // redstone wires to items if the block beneath was removed. + // With this accelerator, RedstoneWireBlock.neighborChanged + // is only typically called for a single wire block, while + // others are processed internally by the breadth first search + // algorithm. To preserve this game behavior, this check must + // be replicated here. + if (!oldState.canSurvive(worldIn, pos)) { + // Pop off the redstone dust + Block.dropResources(oldState, worldIn, pos); + worldIn.removeBlock(pos, false); + + // Mark this position as not being redstone wire + upd1.type = UpdateNode.Type.OTHER; + + // Note: Sending updates to air blocks leads to an empty method. + // Testing shows this to be faster than explicitly avoiding updates to + // air blocks. + return; + } + + // If the above conditions fail, then this is a redstone wire block. + upd1.type = UpdateNode.Type.REDSTONE; + } + + + /* + * Given which redstone wire blocks have been visited and not visited + * around the position currently being updated, compute the cardinal + * direction that is "forward." + * + * rx is the forward direction along the West/East axis + * rz is the forward direction along the North/South axis + */ + static private int computeHeading(final int rx, final int rz) { + // rx and rz can only take on values -1, 0, and 1, so we can + // compute a code number that allows us to use a single switch + // to determine the heading. + final int code = (rx + 1) + 3*(rz + 1); + switch (code) { + case 0: { + // Both rx and rz are -1 (northwest) + // Randomly choose one to be forward. + final int j = ThreadLocalRandom.current().nextInt(0, 1); + return (j==0) ? North : West; + } + case 1: { + // rx=0, rz=-1 + // Definitively North + return North; + } + case 2: { + // rx=1, rz=-1 (northeast) + // Choose randomly between north and east + final int j = ThreadLocalRandom.current().nextInt(0, 1); + return (j==0) ? North : East; + } + case 3: { + // rx=-1, rz=0 + // Definitively West + return West; + } + case 4: { + // rx=0, rz=0 + // Heading is completely ambiguous. Choose + // randomly among the four cardinal directions. + return ThreadLocalRandom.current().nextInt(0, 4); + } + case 5: { + // rx=1, rz=0 + // Definitively East + return East; + } + case 6: { + // rx=-1, rz=1 (southwest) + // Choose randomly between south and west + final int j = ThreadLocalRandom.current().nextInt(0, 1); + return (j==0) ? South : West; + } + case 7: { + // rx=0, rz=1 + // Definitively South + return South; + } + case 8: { + // rx=1, rz=1 (southeast) + // Choose randomly between south and east + final int j = ThreadLocalRandom.current().nextInt(0, 1); + return (j==0) ? South : East; + } + } + + // We should never get here + return ThreadLocalRandom.current().nextInt(0, 4); + } + + // Select whether to use updateSurroundingRedstone from RedstoneWireBlock (old) + // or this helper class (new) + private static final boolean old_current_change = false; + + /* + * Process a node whose neighboring redstone wire has experienced value changes. + */ + private void updateNode(final Level worldIn, final UpdateNode upd1, final int layer) { + final BlockPos pos = upd1.self; + + // Mark this redstone wire as having been visited so that it can be used + // to calculate direction of information flow. + upd1.visited = true; + + // Look up the last known state. + // Due to the way other redstone components are updated, we do not + // have to worry about a state changing behind our backs. The rare + // exception is handled by scheduleReentrantNeighborChanged. + final BlockState oldState = upd1.currentState; + + // Ask the wire block to compute its power level from its neighbors. + // This will also update the wire's power level and return a new + // state if it has changed. When a wire power level is changed, + // calculateCurrentChanges will immediately update the block state in the world + // and return the same value here to be cached in the corresponding + // UpdateNode object. + BlockState newState; + if (old_current_change) { + newState = ((RedstoneWireBlockInterface)wire).updateLogicPublic(worldIn, pos, oldState); + } else { + // Looking up block state is slow. This accelerator includes a version of + // calculateCurrentChanges that uses cahed wire values for a + // significant performance boost. + newState = this.calculateCurrentChanges(worldIn, upd1); + } + + // Only inform neighors if the state has changed + if (newState != oldState) { + // Store the new state + upd1.currentState = newState; + + // Inform neighbors of the change + propagateChanges(worldIn, upd1, layer); + } + } + + /* + * This identifies the neighboring positions of a new UpdateNode object, + * determines their types, and links those to into the graph. Then based on + * what nodes in the redstone wire graph have been visited, the neighbors + * are reordered left-to-right relative to the direction of information flow. + */ + private void findNeighbors(final Level worldIn, final UpdateNode upd1) { + final BlockPos pos = upd1.self; + + // Get the list of neighbor coordinates + final BlockPos[] neighbors = computeAllNeighbors(pos); + + // Temporary array of neighbors in cardinal ordering + final UpdateNode[] neighbor_nodes = new UpdateNode[24]; + + // Target array of neighbors sorted left-to-right + upd1.neighbor_nodes = new UpdateNode[24]; + + for (int i=0; i<24; i++) { + // Look up each neighbor in the node cache + final BlockPos pos2 = neighbors[i]; + UpdateNode upd2 = nodeCache.get(pos2); + if (upd2 == null) { + // If this is a previously unreached position, create + // a new update node, add it to the cache, and identify what it is. + upd2 = new UpdateNode(); + upd2.self = pos2; + upd2.parent = pos; + nodeCache.put(pos2, upd2); + identifyNode(worldIn, upd2); + } + + // For non-redstone blocks, any of the 24 neighboring positions + // should receive a block update. However, some block coordinates + // may contain a redstone wire that does not directly connect to the + // one being expanded. To avoid redundant calculations and confusing + // cross-talk, those neighboring positions are not included. + if (update_redstone[i] || upd2.type != UpdateNode.Type.REDSTONE) { + neighbor_nodes[i] = upd2; + } + } + + // Determine the directions from which the redstone signal may have come from. This + // checks for redstone wire at the same Y level and also Y+1 and Y-1, relative to the + // block being expanded. + final boolean fromWest = (neighbor_nodes[0].visited || neighbor_nodes[7].visited || neighbor_nodes[8].visited); + final boolean fromEast = (neighbor_nodes[1].visited || neighbor_nodes[12].visited || neighbor_nodes[13].visited); + final boolean fromNorth = (neighbor_nodes[4].visited || neighbor_nodes[17].visited || neighbor_nodes[20].visited); + final boolean fromSouth = (neighbor_nodes[5].visited || neighbor_nodes[18].visited || neighbor_nodes[21].visited); + + int cx = 0, cz = 0; + if (fromWest) cx += 1; + if (fromEast) cx -= 1; + if (fromNorth) cz += 1; + if (fromSouth) cz -= 1; + + int heading; + if (cx==0 && cz==0) { + // If there is no clear direction, try to inherit the heading from ancestor nodes. + heading = computeHeading(upd1.xbias, upd1.zbias); + + // Propagate that heading to descendent nodes. + for (int i=0; i<24; i++) { + final UpdateNode nn = neighbor_nodes[i]; + if (nn != null) { + nn.xbias = upd1.xbias; + nn.zbias = upd1.zbias; + } + } + } else { + if (cx != 0 && cz != 0) { + // If the heading is somewhat ambiguous, try to disambiguate based on + // ancestor nodes. + if (upd1.xbias != 0) cz = 0; + if (upd1.zbias != 0) cx = 0; + } + heading = computeHeading(cx, cz); + + // Propagate that heading to descendent nodes. + for (int i=0; i<24; i++) { + final UpdateNode nn = neighbor_nodes[i]; + if (nn != null) { + nn.xbias = cx; + nn.zbias = cz; + } + } + } + + // Reorder neighboring UpdateNode objects according to the forward direction + // determined above. + orientNeighbors(neighbor_nodes, upd1.neighbor_nodes, heading); + } + + /* + * For any redstone wire block in layer N, inform neighbors to recompute their states + * in layers N+1 and N+2; + */ + private void propagateChanges(final Level worldIn, final UpdateNode upd1, final int layer) { + if (upd1.neighbor_nodes == null) { + // If this node has not been expanded yet, find its neigbors + findNeighbors(worldIn, upd1); + } + + final BlockPos pos = upd1.self; + final int x = pos.getX(); + final int y = pos.getY(); + final int z = pos.getZ(); + + // Make sure there are enough layers in the list + //while (updateLayers.size() <= layer+2) updateLayers.add(new ArrayList()); + + // All neighbors may be scheduled for layer N+1 + final int layer1 = layer + 1; + + // If the node being updated (upd1) has already been expanded, then merely + // schedule updates to its neighbors. + for (int i=0; i<24; i++) { + final UpdateNode upd2 = upd1.neighbor_nodes[i]; + + // This test ensures that an UpdateNode is never scheduled to the same layer + // more than once. Also, skip non-connecting redstone wire blocks + if (upd2 != null && layer1 > upd2.layer) { + upd2.layer = layer1; + updateQueue1.add(upd2); + + // Keep track of which block updated this neighbor + upd2.parent = pos; + } + } + + // Nodes above and below are scheduled ALSO for layer N+2 + final int layer2 = layer + 2; + + // Repeat of the loop above, but only for the first four (above and below) neighbors + // and for layer N+2; + for (int i=0; i<4; i++) { + final UpdateNode upd2 = upd1.neighbor_nodes[i]; + if (upd2 != null && layer2 > upd2.layer) { + upd2.layer = layer2; + updateQueue2.add(upd2); + upd2.parent = pos; + } + } + } + + + // The breadth-first search below will send block updates to blocks + // that are not redstone wire. If one of those updates results in + // a distant redstone wire getting an update, then this.neighborChanged + // will get called. This would be a reentrant call, and + // it is necessary to properly integrate those updates into the + // on-going search through redstone wire. Thus, we make the layer + // currently being processed visible at the object level. + + // The current layer being processed by the breadth-first search + private int currentWalkLayer = 0; + + private void shiftQueue() { + final List t = updateQueue0; + t.clear(); + updateQueue0 = updateQueue1; + updateQueue1 = updateQueue2; + updateQueue2 = t; + } + + /* + * Perform a breadth-first (layer by layer) traversal through redstone + * wire blocks, propagating value changes to neighbors in an order + * that is a function of distance from the initial call to + * this.neighborChanged. + */ + private void breadthFirstWalk(final Level worldIn) { + shiftQueue(); + currentWalkLayer = 1; + + // Loop over all layers + while (updateQueue0.size()>0 || updateQueue1.size()>0) { + // Get the set of blocks in this layer + final List thisLayer = updateQueue0; + //if (thisLayer.size() < 1) { + //shiftQueue(); + //currentWalkLayer++; + //continue; + //} + + // Loop over all blocks in the layer. Recall that + // this is a List, preserving the insertion order of + // left-to-right based on direction of information flow. + for (UpdateNode upd : thisLayer) { + if (upd.type == UpdateNode.Type.REDSTONE) { + // If the node is is redstone wire, + // schedule updates to neighbors if its value + // has changed. + updateNode(worldIn, upd, currentWalkLayer); + } else { + // If this block is not redstone wire, send a block update. + // Redstone wire blocks get state updates, but they don't + // need block updates. Only non-redstone neighbors need updates. + + // The following function is called "neighborChanged" in the latest + // deobfuscated names. World.func_190524_a is called from + // World.notifyNeighborsOfStateChange, and + // notifyNeighborsOfStateExcept. We don't use + // World.notifyNeighborsOfStateChange here, since we are + // already keeping track of all of the neighbor positions + // that need to be updated. All on its own, handling neighbors + // this way reduces block updates by 1/3 (24 instead of 36). +// worldIn.neighborChanged(upd.self, wire, upd.parent); + + // [Space Walker] + // The neighbor update system got a significant overhaul in 1.19. + // Shape and block updates are now added to a stack before being + // processed. These changes make it so any neighbor updates emitted + // by this accelerator will not be processed until after the entire + // wire network has updated. This has a significant impact on the + // behavior and introduces Vanilla parity issues. + // To circumvent this issue we bypass the neighbor update stack and + // call BlockStateBase#neighborChanged directly. This change mostly + // restores old behavior, at the cost of bypassing the + // max-chained-neighbor-updates server property. + worldIn.getBlockState(upd.self).handleNeighborChanged(worldIn, upd.self, wire, upd.parent, false); + } + } + + // Move on to the next layer + shiftQueue(); + currentWalkLayer++; + } + + currentWalkLayer = 0; + } + + + /* + * Normally, when Minecraft is computing redstone wire power changes, and a wire power level + * change sends a block update to a neighboring functional component (e.g. piston, repeater, etc.), + * those updates are queued. Only once all redstone wire updates are complete will any component + * action generate any further block updates to redstone wire. Instant repeater lines, for instance, + * will process all wire updates for one redstone line, after which the pistons will zero-tick, + * after which the next redstone line performs all of its updates. Thus, each wire is processed in its + * own discrete wave. + * + * However, there are some corner cases where this pattern breaks, with a proof of concept discovered + * by Rays Works, which works the same in vanilla. The scenario is as follows: + * (1) A redstone wire is conducting a signal. + * (2) Partway through that wave of updates, a neighbor is updated that causes an update to a completely + * separate redstone wire. + * (3) This results in a call to RedstoneWireBlock.neighborChanged for that other wire, in the middle of + * an already on-going propagation through the first wire. + * + * The vanilla code, being depth-first, would end up fully processing the second wire before going back + * to finish processing the first one. (Although technically, vanilla has no special concept of "being + * in the middle" of processing updates to a wire.) For the breadth-first algorithm, we give this + * situation special handling, where the updates for the second wire are incorporated into the schedule + * for the first wire, and then the callstack is allowed to unwind back to the on-going search loop in + * order to continue processing both the first and second wire in the order of distance from the initial + * trigger. + */ + private BlockState scheduleReentrantNeighborChanged(final Level worldIn, final BlockPos pos, final BlockState newState, final BlockPos source) + { + if (source != null) { + // If the cause of the redstone wire update is known, we can use that to help determine + // direction of information flow. + UpdateNode src = nodeCache.get(source); + if (src == null) { + src = new UpdateNode(); + src.self = source; + src.parent = source; + src.visited = true; + identifyNode(worldIn, src); + nodeCache.put(source, src); + } + } + + // Find or generate a node for the redstone block position receiving the update + UpdateNode upd = nodeCache.get(pos); + if (upd == null) { + upd = new UpdateNode(); + upd.self = pos; + upd.parent = pos; + upd.visited = true; + identifyNode(worldIn, upd); + nodeCache.put(pos, upd); + } + upd.currentState = newState; + + // Receiving this block update may mean something in the world changed. + // Therefore we clear the cached block info about all neighbors of + // the position receiving the update and then re-identify what they are. + if (upd.neighbor_nodes != null) { + for (int i=0; i<24; i++) { + final UpdateNode upd2 = upd.neighbor_nodes[i]; + if (upd2 == null) continue; + upd2.type = UpdateNode.Type.UNKNOWN; + upd2.currentState = null; + identifyNode(worldIn, upd2); + } + } + + // The block at 'pos' is a redstone wire and has been updated already by calling + // wire.calculateCurrentChanges, so we don't schedule that. However, we do need + // to schedule its neighbors. By passing the current value of 'currentWalkLayer' to + // propagateChanges, the neighbors of 'pos' are schedule for layers currentWalkLayer+1 + // and currentWalkLayer+2. + propagateChanges(worldIn, upd, currentWalkLayer); + + // Return here. The call stack will unwind back to the first call to + // updateSurroundingRedstone, whereupon the new updates just scheduled will + // be propagated. This also facilitates elimination of superfluous and + // redundant block updates. + return newState; + } + + /* + * New version of pre-existing updateSurroundingRedstone, which is called from + * wire.updateSurroundingRedstone, which is called from wire.neighborChanged and a + * few other methods in RedstoneWireBlock. This sets off the breadth-first + * walk through all redstone dust connected to the initial position triggered. + */ + public BlockState updateSurroundingRedstone(final Level worldIn, final BlockPos pos, final BlockState state, final BlockPos source) + { + // Check this block's neighbors and see if its power level needs to change + // Use the calculateCurrentChanges method in RedstoneWireBlock since we have no + // cached block states at this point. + final BlockState newState = ((RedstoneWireBlockInterface)wire).updateLogicPublic(worldIn, pos, state); + + // If no change, exit + if (newState == state) { + return state; + } + + // Check to see if this update was received during an on-going breadth first search + if (currentWalkLayer>0 || nodeCache.size()>0) { + // As breadthFirstWalk progresses, it sends block updates to neighbors. Some of those + // neighbors may affect the world so as to cause yet another redstone wire block to receive + // an update. If that happens, we need to integrate those redstone wire updates into the + // already on-going graph walk being performed by breadthFirstWalk. + return scheduleReentrantNeighborChanged(worldIn, pos, newState, source); + } + // If there are no on-going walks through redstone wire, then start a new walk. + + // If the source of the block update to the redstone wire at 'pos' is known, we can use + // that to help determine the direction of information flow. + if (source != null) { + final UpdateNode src = new UpdateNode(); + src.self = source; + src.parent = source; + src.visited = true; + nodeCache.put(source, src); + identifyNode(worldIn, src); + } + + // Create a node representing the block at 'pos', and then propagate updates + // to its neighbors. As stated above, the call to wire.calculateCurrentChanges + // already performs the update to the block at 'pos', so it is not added to the schedule. + final UpdateNode upd = new UpdateNode(); + upd.self = pos; + upd.parent = source!=null ? source : pos; + upd.currentState = newState; + upd.type = UpdateNode.Type.REDSTONE; + upd.visited = true; + nodeCache.put(pos, upd); + propagateChanges(worldIn, upd, 0); + + // Perform the walk over all directly reachable redstone wire blocks, propagating wire value + // updates in a breadth first order out from the initial update received for the block at 'pos'. + breadthFirstWalk(worldIn); + + // With the whole search completed, clear the list of all known blocks. + // We do not want to keep around state information that may be changed by other code. + // In theory, we could cache the neighbor block positions, but that is a separate + // optimization. + nodeCache.clear(); + + return newState; + } + + + // For any array of neighbors in an UpdateNode object, these are always + // the indices of the four immediate neighbors at the same Y coordinate. + private static final int[] rs_neighbors = {4, 5, 6, 7}; + private static final int[] rs_neighbors_up = {9, 11, 13, 15}; + private static final int[] rs_neighbors_dn = {8, 10, 12, 14}; + + /* + * Updated calculateCurrentChanges that is optimized for speed and uses + * the UpdateNode's neighbor array to find the redstone states of neighbors + * that might power it. + */ + private BlockState calculateCurrentChanges(final Level worldIn, final UpdateNode upd) + { + BlockState state = upd.currentState; + final int i = state.getValue(RedStoneWireBlock.POWER); + int j = 0; + j = getMaxCurrentStrength(upd, j); + int l = 0; + + ((RedstoneWireBlockInterface)wire).setWiresGivePower(false); + // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, + // and I'm not ready to try to replicate even more functionality from + // elsewhere in Minecraft into this accelerator. So sadly, we must + // suffer the performance hit of this very expensive call. If there + // is consistency to what this call returns, we may be able to cache it. + final int k = worldIn.getBestNeighborSignal(upd.self); + ((RedstoneWireBlockInterface)wire).setWiresGivePower(true); + + // The variable 'k' holds the maximum redstone power value of any adjacent blocks. + // If 'k' has the highest level of all neighbors, then the power level of this + // redstone wire will be set to 'k'. If 'k' is already 15, then nothing inside the + // following loop can affect the power level of the wire. Therefore, the loop is + // skipped if k is already 15. + if (k<15) { + if (upd.neighbor_nodes == null) { + // If this node's neighbors are not known, expand the node + findNeighbors(worldIn, upd); + } + + // These remain constant, so pull them out of the loop. + // Regardless of which direction is forward, the UpdateNode for the + // position directly above the node being calculated is always + // at index 1. + UpdateNode center_up = upd.neighbor_nodes[1]; + boolean center_up_is_cube = center_up.currentState.isRedstoneConductor(worldIn, center_up.self); //isSimpleFUllBLock + + for (int m=0; m<4; m++) { + // Get the neighbor array index of each of the four cardinal + // neighbors. + int n = rs_neighbors[m]; + + // Get the max redstone power level of each of the cardinal + // neighbors + UpdateNode neighbor = upd.neighbor_nodes[n]; + l = getMaxCurrentStrength(neighbor, l); + + // Also check the positions above and below the cardinal + // neighbors + boolean neighbor_is_cube = neighbor.currentState.isRedstoneConductor(worldIn, neighbor.self); //isSimpleFUllBLock + if (!neighbor_is_cube) { + UpdateNode neighbor_down = upd.neighbor_nodes[rs_neighbors_dn[m]]; + l = getMaxCurrentStrength(neighbor_down, l); + } else + if (!center_up_is_cube) { + UpdateNode neighbor_up = upd.neighbor_nodes[rs_neighbors_up[m]]; + l = getMaxCurrentStrength(neighbor_up, l); + } + } + } + + // The new code sets this redstonewire block's power level to the highest neighbor + // minus 1. This usually results in wire power levels dropping by 2 at a time. + // This optimization alone has no impact on opdate order, only the number of updates. + j = l-1; + + // If 'l' turns out to be zero, then j will be set to -1, but then since 'k' will + // always be in the range of 0 to 15, the following if will correct that. + if (k>j) j=k; + + if (i != j) { + // If the power level has changed from its previous value, compute a new state + // and set it in the world. + // Possible optimization: Don't commit state changes to the world until they + // need to be known by some nearby non-redstone-wire block. + state = state.setValue(RedStoneWireBlock.POWER, j); + // [gnembon] added state check cause other things in the tick may have popped it up already + // https://github.com/gnembon/fabric-carpet/issues/117 + if (worldIn.getBlockState(upd.self).getBlock() == Blocks.REDSTONE_WIRE) + // [Space Walker] suppress shape updates and emit those manually to + // bypass the new neighbor update stack. + if (worldIn.setBlock(upd.self, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) + updateNeighborShapes(worldIn, upd.self, state); + } + + return state; + } + + /* + * [Space Walker] + * This method emits shape updates around the given block, + * bypassing the new neighbor update stack. Diagonal shape + * updates are omitted, as they are mostly unnecessary. + * Diagonal shape updates are emitted exclusively to other + * redstone wires, in order to update their connection properties. + * Wire connections should never change as a result of power + * changes, so the only behavioral change will be in scenarios + * where earlier shape updates have been suppressed to keep a + * redstone wire in an invalid state. + */ + public void updateNeighborShapes(Level level, BlockPos pos, BlockState state) { + // these updates will be added to the stack and processed after the entire network has updated + state.updateIndirectNeighbourShapes(level, pos, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS); + + for (Direction dir : Block.UPDATE_SHAPE_ORDER) { + BlockPos neighborPos = pos.relative(dir); + BlockState neighborState = level.getBlockState(neighborPos); + + BlockState newState = neighborState.updateShape(dir.getOpposite(), state, level, neighborPos, pos); + Block.updateOrDestroy(neighborState, newState, level, neighborPos, Block.UPDATE_CLIENTS); + } + } + + /* + * Optimized function to compute a redstone wire's power level based on cached + * state. + */ + private static int getMaxCurrentStrength(final UpdateNode upd, final int strength) { + if (upd.type != UpdateNode.Type.REDSTONE) return strength; + final int i = upd.currentState.getValue(RedStoneWireBlock.POWER); + return i > strength ? i : strength; + } +} \ No newline at end of file diff --git a/src/main/java/carpet/logging/HUDController.java b/src/main/java/carpet/logging/HUDController.java new file mode 100644 index 0000000..b29fbab --- /dev/null +++ b/src/main/java/carpet/logging/HUDController.java @@ -0,0 +1,149 @@ +package carpet.logging; + +import carpet.CarpetServer; +import carpet.helpers.HopperCounter; +import carpet.logging.logHelpers.PacketCounter; +import carpet.utils.Messenger; +import carpet.utils.SpawnReporter; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundTabListPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerTickRateManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.TimeUtil; +import net.minecraft.world.level.Level; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +public class HUDController +{ + private static final List> HUDListeners = new ArrayList<>(); + + /** + * Adds listener to be called when HUD is updated for logging information + * @param listener - a method to be called when new HUD inforation are collected + */ + public static void register(Consumer listener) + { + HUDListeners.add(listener); + } + + public static final Map> player_huds = new HashMap<>(); +//keyed with player names so unlogged players don't hold the reference + public static final Map scarpet_headers = new HashMap<>(); + + public static final Map scarpet_footers = new HashMap<>(); + + public static void resetScarpetHUDs() { + scarpet_headers.clear(); + scarpet_footers.clear(); + } + + public static void addMessage(ServerPlayer player, Component hudMessage) + { + if (player == null) return; + if (!player_huds.containsKey(player)) + { + player_huds.put(player, new ArrayList<>()); + } + else + { + player_huds.get(player).add(Component.literal("\n")); + } + player_huds.get(player).add(hudMessage); + } + + public static void clearPlayer(ServerPlayer player) + { + ClientboundTabListPacket packet = new ClientboundTabListPacket(Component.literal(""), Component.literal("")); + player.connection.send(packet); + } + + + public static void update_hud(MinecraftServer server, List force) + { + if (((server.getTickCount() % 20 != 0) && force == null) || CarpetServer.minecraft_server == null) + return; + + player_huds.clear(); + + server.getPlayerList().getPlayers().forEach(p -> { + Component scarpetFOoter = scarpet_footers.get(p.getScoreboardName()); + if (scarpetFOoter != null) HUDController.addMessage(p, scarpetFOoter); + }); + + if (LoggerRegistry.__tps) + LoggerRegistry.getLogger("tps").log(()-> send_tps_display(server)); + + if (LoggerRegistry.__mobcaps) + LoggerRegistry.getLogger("mobcaps").log((option, player) -> { + ResourceKey dim = switch (option) { + case "overworld" -> Level.OVERWORLD; + case "nether" -> Level.NETHER; + case "end" -> Level.END; + default -> player.level().dimension(); + }; + return new Component[]{SpawnReporter.printMobcapsForDimension(server.getLevel(dim), false).get(0)}; + }); + + if(LoggerRegistry.__counter) + LoggerRegistry.getLogger("counter").log((option)->send_counter_info(server, option)); + + if (LoggerRegistry.__packets) + LoggerRegistry.getLogger("packets").log(HUDController::packetCounter); + + // extensions have time to pitch in. + HUDListeners.forEach(l -> l.accept(server)); + + Set targets = new HashSet<>(player_huds.keySet()); + if (force!= null) targets.addAll(force); + for (ServerPlayer player: targets) + { + ClientboundTabListPacket packet = new ClientboundTabListPacket( + scarpet_headers.getOrDefault(player.getScoreboardName(), Component.literal("")), + Messenger.c(player_huds.getOrDefault(player, List.of()).toArray(new Object[0])) + ); + player.connection.send(packet); + } + } + private static Component [] send_tps_display(MinecraftServer server) + { + double MSPT = ((double)server.getAverageTickTimeNanos())/ TimeUtil.NANOSECONDS_PER_MILLISECOND; + ServerTickRateManager trm = server.tickRateManager(); + + double TPS = 1000.0D / Math.max(trm.isSprinting()?0.0:trm.millisecondsPerTick(), MSPT); + if (trm.isFrozen()) { + TPS = 0; + } + String color = Messenger.heatmap_color(MSPT,trm.millisecondsPerTick()); + return new Component[]{Messenger.c( + "g TPS: ", String.format(Locale.US, "%s %.1f",color, TPS), + "g MSPT: ", String.format(Locale.US,"%s %.1f", color, MSPT))}; + } + + private static Component[] send_counter_info(MinecraftServer server, String colors) + { + List res = new ArrayList<>(); + for (String color : colors.split(",")) + { + HopperCounter counter = HopperCounter.getCounter(color); + if (counter != null) res.addAll(counter.format(server, false, true)); + } + return res.toArray(new Component[0]); + } + private static Component [] packetCounter() + { + Component [] ret = new Component[]{ + Messenger.c("w I/" + PacketCounter.totalIn + " O/" + PacketCounter.totalOut), + }; + PacketCounter.reset(); + return ret; + } +} diff --git a/src/main/java/carpet/logging/HUDLogger.java b/src/main/java/carpet/logging/HUDLogger.java new file mode 100644 index 0000000..57e1e64 --- /dev/null +++ b/src/main/java/carpet/logging/HUDLogger.java @@ -0,0 +1,53 @@ +package carpet.logging; + +import java.lang.reflect.Field; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +public class HUDLogger extends Logger +{ + static Logger stardardHUDLogger(String logName, String def, String [] options) + { + return stardardHUDLogger(logName, def, options, false); + } + + static Logger stardardHUDLogger(String logName, String def, String [] options, boolean strictOptions) + { + // should convert to factory method if more than 2 classes are here + try + { + return new HUDLogger(LoggerRegistry.class.getField("__"+logName), logName, def, options, strictOptions); + } + catch (NoSuchFieldException e) + { + throw new RuntimeException("Failed to create logger "+logName); + } + } + + public HUDLogger(Field field, String logName, String def, String[] options, boolean strictOptions) + { + super(field, logName, def, options, strictOptions); + } + + @Deprecated + public HUDLogger(Field field, String logName, String def, String[] options) { + super(field, logName, def, options, false); + } + + @Override + public void removePlayer(String playerName) + { + ServerPlayer player = playerFromName(playerName); + if (player != null) HUDController.clearPlayer(player); + super.removePlayer(playerName); + } + + @Override + public void sendPlayerMessage(ServerPlayer player, Component... messages) + { + for (Component m:messages) HUDController.addMessage(player, m); + } + + +} diff --git a/src/main/java/carpet/logging/Logger.java b/src/main/java/carpet/logging/Logger.java new file mode 100644 index 0000000..86cf5c9 --- /dev/null +++ b/src/main/java/carpet/logging/Logger.java @@ -0,0 +1,255 @@ +package carpet.logging; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import net.minecraft.Util; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +public class Logger +{ + // The set of subscribed and online players. + private Map subscribedOnlinePlayers; + + // The set of subscribed and offline players. + private Map subscribedOfflinePlayers; + + // The logName of this log. Gets prepended to logged messages. + private String logName; + + private String default_option; + + private String[] options; + + private Field acceleratorField; + + private boolean strictOptions; + + static Logger stardardLogger(String logName, String def, String [] options) + { + return stardardLogger(logName, def, options, false); + } + + static Logger stardardLogger(String logName, String def, String [] options, boolean strictOptions) + { + try + { + return new Logger(LoggerRegistry.class.getField("__"+logName), logName, def, options, strictOptions); + } + catch (NoSuchFieldException e) + { + throw new RuntimeException("Failed to create logger "+logName); + } + } + + @Deprecated + public Logger(Field acceleratorField, String logName, String def, String [] options) { + this(acceleratorField, logName, def, options, false); + } + + public Logger(Field acceleratorField, String logName, String def, String [] options, boolean strictOptions) + { + subscribedOnlinePlayers = new HashMap<>(); + subscribedOfflinePlayers = new HashMap<>(); + this.acceleratorField = acceleratorField; + this.logName = logName; + this.default_option = def; + this.options = options == null ? new String[0] : options; + this.strictOptions = strictOptions; + if (acceleratorField == null) + CarpetSettings.LOG.error("[CM] Logger "+getLogName()+" is missing a specified accelerator"); + } + + public String getDefault() + { + return default_option; + } + public String [] getOptions() + { + return options; + } + public String getLogName() + { + return logName; + } + + /** + * Subscribes the player with the given logName to the logger. + */ + public void addPlayer(String playerName, String option) + { + if (playerFromName(playerName) != null) + { + subscribedOnlinePlayers.put(playerName, option); + } + else + { + subscribedOfflinePlayers.put(playerName, option); + } + LoggerRegistry.setAccess(this); + } + + /** + * Unsubscribes the player with the given logName from the logger. + */ + public void removePlayer(String playerName) + { + subscribedOnlinePlayers.remove(playerName); + subscribedOfflinePlayers.remove(playerName); + LoggerRegistry.setAccess(this); + } + + /** + * Returns true if there are any online subscribers for this log. + */ + public boolean hasOnlineSubscribers() + { + return subscribedOnlinePlayers.size() > 0; + } + + public void serverStopped() + { + subscribedOnlinePlayers.clear(); + subscribedOfflinePlayers.clear(); + } + + public Field getField() + { + return acceleratorField; + } + + /** + * serves messages to players fetching them from the promise + * will repeat invocation for players that share the same option + */ + @FunctionalInterface + public interface lMessage { Component [] get(String playerOption, Player player);} + public void log(lMessage messagePromise) + { + for (Map.Entry en : subscribedOnlinePlayers.entrySet()) + { + ServerPlayer player = playerFromName(en.getKey()); + if (player != null) + { + Component [] messages = messagePromise.get(en.getValue(),player); + if (messages != null) + sendPlayerMessage(player, messages); + } + } + } + + /** + * guarantees that each message for each option will be evaluated once from the promise + * and served the same way to all other players subscribed to the same option + */ + @FunctionalInterface + public interface lMessageIgnorePlayer { Component [] get(String playerOption);} + public void log(lMessageIgnorePlayer messagePromise) + { + Map cannedMessages = new HashMap<>(); + for (Map.Entry en : subscribedOnlinePlayers.entrySet()) + { + ServerPlayer player = playerFromName(en.getKey()); + if (player != null) + { + String option = en.getValue(); + if (!cannedMessages.containsKey(option)) + { + cannedMessages.put(option,messagePromise.get(option)); + } + Component [] messages = cannedMessages.get(option); + if (messages != null) + sendPlayerMessage(player, messages); + } + } + } + /** + * guarantees that message is evaluated once, so independent from the player and chosen option + */ + public void log(Supplier messagePromise) + { + Component [] cannedMessages = null; + for (Map.Entry en : subscribedOnlinePlayers.entrySet()) + { + ServerPlayer player = playerFromName(en.getKey()); + if (player != null) + { + if (cannedMessages == null) cannedMessages = messagePromise.get(); + sendPlayerMessage(player, cannedMessages); + } + } + } + + public void sendPlayerMessage(ServerPlayer player, Component ... messages) + { + Arrays.stream(messages).forEach(player::sendSystemMessage); + } + + /** + * Gets the {@code PlayerEntity} instance for a player given their UUID. Returns null if they are offline. + */ + protected ServerPlayer playerFromName(String name) + { + return CarpetServer.minecraft_server.getPlayerList().getPlayerByName(name); + } + + // ----- Event Handlers ----- // + + public void onPlayerConnect(Player player, boolean firstTime) + { + // If the player was subscribed to the log and offline, move them to the set of online subscribers. + String playerName = player.getName().getString(); + if (subscribedOfflinePlayers.containsKey(playerName)) + { + subscribedOnlinePlayers.put(playerName, subscribedOfflinePlayers.get(playerName)); + subscribedOfflinePlayers.remove(playerName); + } + else if(firstTime) + { + Set loggingOptions = new HashSet<>(Arrays.asList(CarpetSettings.defaultLoggers.split(","))); + String logName = getLogName(); + for (String str : loggingOptions) { + String[] vars = str.split(" ", 2); + if (vars[0].equals(logName)) { + LoggerRegistry.subscribePlayer(playerName, getLogName(), vars.length == 1 ? getDefault() : vars[1]); + break; + } + } + } + LoggerRegistry.setAccess(this); + } + + public void onPlayerDisconnect(Player player) + { + // If the player was subscribed to the log, move them to the set of offline subscribers. + String playerName = player.getName().getString(); + if (subscribedOnlinePlayers.containsKey(playerName)) + { + subscribedOfflinePlayers.put(playerName, subscribedOnlinePlayers.get(playerName)); + subscribedOnlinePlayers.remove(playerName); + } + LoggerRegistry.setAccess(this); + } + + public String getAcceptedOption(String arg) + { + if (Arrays.asList(this.getOptions()).contains(arg)) return arg; + return null; + } + + public boolean isOptionValid(String option) { + if (strictOptions) + { + return Arrays.asList(this.getOptions()).contains(option); + } + return option != null; + } +} diff --git a/src/main/java/carpet/logging/LoggerRegistry.java b/src/main/java/carpet/logging/LoggerRegistry.java new file mode 100644 index 0000000..0b448e2 --- /dev/null +++ b/src/main/java/carpet/logging/LoggerRegistry.java @@ -0,0 +1,173 @@ +package carpet.logging; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.DyeColor; + +public class LoggerRegistry +{ + // Map from logger names to loggers. + private static final Map loggerRegistry = new HashMap<>(); + // Map from player names to the set of names of the logs that player is subscribed to. + private static final Map> playerSubscriptions = new HashMap<>(); + //statics to quickly asses if its worth even to call each one + public static boolean __tnt; + public static boolean __projectiles; + public static boolean __fallingBlocks; + public static boolean __tps; + public static boolean __counter; + public static boolean __mobcaps; + public static boolean __packets; + public static boolean __pathfinding; + public static boolean __explosions; + + public static void initLoggers() + { + stopLoggers(); + registerLoggers(); + CarpetServer.registerExtensionLoggers(); + } + + public static void registerLoggers() + { + registerLogger("tnt", Logger.stardardLogger( "tnt", "brief", new String[]{"brief", "full"}, true)); + registerLogger("projectiles", Logger.stardardLogger("projectiles", "brief", new String[]{"brief", "full"})); + registerLogger("fallingBlocks",Logger.stardardLogger("fallingBlocks", "brief", new String[]{"brief", "full"})); + registerLogger("pathfinding", Logger.stardardLogger("pathfinding", "20", new String[]{"2", "5", "10"})); + registerLogger("tps", HUDLogger.stardardHUDLogger("tps", null, null)); + 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", Logger.stardardLogger("explosions", "brief",new String[]{"brief", "full"}, true)); + + } + + /** + * Gets the logger with the given name. Returns null if no such logger exists. + */ + public static Logger getLogger(String name) { return loggerRegistry.get(name); } + + /** + * Gets the set of logger names. + */ + public static Set getLoggerNames() { return loggerRegistry.keySet(); } + + /** + * Subscribes the player with name playerName to the log with name logName. + */ + public static void subscribePlayer(String playerName, String logName, String option) + { + if (!playerSubscriptions.containsKey(playerName)) playerSubscriptions.put(playerName, new HashMap<>()); + Logger log = loggerRegistry.get(logName); + if (option == null) option = log.getDefault(); + playerSubscriptions.get(playerName).put(logName,option); + log.addPlayer(playerName, option); + } + + /** + * Unsubscribes the player with name playerName from the log with name logName. + */ + public static void unsubscribePlayer(String playerName, String logName) + { + if (playerSubscriptions.containsKey(playerName)) + { + Map subscriptions = playerSubscriptions.get(playerName); + subscriptions.remove(logName); + loggerRegistry.get(logName).removePlayer(playerName); + if (subscriptions.size() == 0) playerSubscriptions.remove(playerName); + } + } + + /** + * If the player is not subscribed to the log, then subscribe them. Otherwise, unsubscribe them. + */ + public static boolean togglePlayerSubscription(String playerName, String logName) + { + if (playerSubscriptions.containsKey(playerName) && playerSubscriptions.get(playerName).containsKey(logName)) + { + unsubscribePlayer(playerName, logName); + return false; + } + else + { + subscribePlayer(playerName, logName, null); + return true; + } + } + + /** + * Get the set of logs the current player is subscribed to. + */ + public static Map getPlayerSubscriptions(String playerName) + { + if (playerSubscriptions.containsKey(playerName)) + { + return playerSubscriptions.get(playerName); + } + return null; + } + + protected static void setAccess(Logger logger) + { + boolean value = logger.hasOnlineSubscribers(); + try + { + Field f = logger.getField(); + f.setBoolean(null, value); + } + catch (IllegalAccessException e) + { + CarpetSettings.LOG.error("Cannot change logger quick access field"); + } + } + /** + * Called when the server starts. Creates the logs used by Carpet mod. + */ + public static void registerLogger(String name, Logger logger) + { + loggerRegistry.put(name, logger); + setAccess(logger); + } + + private final static Set seenPlayers = new HashSet<>(); + + public static void stopLoggers() + { + for(Logger log: loggerRegistry.values() ) + { + log.serverStopped(); + } + seenPlayers.clear(); + loggerRegistry.clear(); + playerSubscriptions.clear(); + } + public static void playerConnected(Player player) + { + boolean firstTime = false; + if (!seenPlayers.contains(player.getName().getString())) + { + seenPlayers.add(player.getName().getString()); + firstTime = true; + //subscribe them to the defualt loggers + } + for(Logger log: loggerRegistry.values() ) + { + log.onPlayerConnect(player, firstTime); + } + } + + public static void playerDisconnected(Player player) + { + for(Logger log: loggerRegistry.values() ) + { + log.onPlayerDisconnect(player); + } + } +} 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 0000000..972ec1a --- /dev/null +++ b/src/main/java/carpet/logging/logHelpers/ExplosionLogHelper.java @@ -0,0 +1,106 @@ +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 java.util.ArrayList; +import java.util.List; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.phys.Vec3; + +import static carpet.utils.Messenger.c; + +public class ExplosionLogHelper +{ + private final boolean createFire; + private final Explosion.BlockInteraction blockDestructionType; + private final RegistryAccess regs; + public final Vec3 pos; + private final float power; + private boolean affectBlocks = false; + private final Object2IntMap impactedEntities = new Object2IntOpenHashMap<>(); + + private static long lastGametime = 0; + private static int explosionCountInCurrentGT = 0; + private static boolean newTick; + + public ExplosionLogHelper(double x, double y, double z, float power, boolean createFire, Explosion.BlockInteraction blockDestructionType, RegistryAccess regs) { + this.power = power; + this.pos = new Vec3(x,y,z); + this.createFire = createFire; + this.blockDestructionType = blockDestructionType; + this.regs = regs; + } + + public void setAffectBlocks(boolean b) + { + affectBlocks = b; + } + + public void onExplosionDone(long gametime) + { + newTick = false; + if (!(lastGametime == gametime)){ + explosionCountInCurrentGT = 0; + lastGametime = gametime; + newTick = true; + } + explosionCountInCurrentGT++; + LoggerRegistry.getLogger("explosions").log( (option) -> { + List messages = new ArrayList<>(); + if(newTick) messages.add(c("wb tick : ", "d " + gametime)); + if ("brief".equals(option)) + { + messages.add( c("d #" + explosionCountInCurrentGT,"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 #" + explosionCountInCurrentGT,"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 "+ regs.registryOrThrow(Registries.ENTITY_TYPE).getKey(k.type).getPath(), (v>1)?"l ("+v+")":"" + )); + }); + } + } + return messages.toArray(new Component[0]); + }); + } + + public void onEntityImpacted(Entity entity, Vec3 accel) + { + EntityChangedStatusWithCount ent = new EntityChangedStatusWithCount(entity, accel); + impactedEntities.put(ent, impactedEntities.getOrDefault(ent, 0)+1); + } + + + public static record EntityChangedStatusWithCount(Vec3 pos, EntityType type, Vec3 accel) + { + public EntityChangedStatusWithCount(Entity e, Vec3 accel) + { + this(e.position(), e.getType(), accel); + } + } +} diff --git a/src/main/java/carpet/logging/logHelpers/PacketCounter.java b/src/main/java/carpet/logging/logHelpers/PacketCounter.java new file mode 100644 index 0000000..e5ca6cc --- /dev/null +++ b/src/main/java/carpet/logging/logHelpers/PacketCounter.java @@ -0,0 +1,8 @@ +package carpet.logging.logHelpers; + +public class PacketCounter +{ + public static long totalOut=0; + public static long totalIn=0; + public static void reset() {totalIn = 0L; totalOut = 0L; } +} diff --git a/src/main/java/carpet/logging/logHelpers/PathfindingVisualizer.java b/src/main/java/carpet/logging/logHelpers/PathfindingVisualizer.java new file mode 100644 index 0000000..ff66ee3 --- /dev/null +++ b/src/main/java/carpet/logging/logHelpers/PathfindingVisualizer.java @@ -0,0 +1,40 @@ +package carpet.logging.logHelpers; + +import carpet.helpers.ParticleDisplay; +import carpet.logging.LoggerRegistry; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +public class PathfindingVisualizer +{ + public static void slowPath(Entity entity, Vec3 target, float miliseconds, boolean successful) + { + if (!LoggerRegistry.__pathfinding) return; + LoggerRegistry.getLogger("pathfinding").log( (option, player)-> + { + if (!(player instanceof ServerPlayer)) + return null; + int minDuration; + try + { + minDuration = Integer.parseInt(option); + } + catch (NumberFormatException ignored) + { + return null; + } + if (miliseconds < minDuration) + return null; + if (player.distanceToSqr(entity) > 1000 && player.distanceToSqr(target) > 1000) + return null; + if (minDuration < 1) + minDuration = 1; + + String accent = successful ? "happy_villager" : "angry_villager"; + String color = (miliseconds/minDuration < 2)? "dust 1 1 0 1" : ((miliseconds/minDuration < 4)?"dust 1 0.5 0 1":"dust 1 0 0 1"); + ParticleDisplay.drawParticleLine((ServerPlayer) player, entity.position(), target, color, accent, 5, 0.5); + return null; + }); + } +} diff --git a/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java b/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java new file mode 100644 index 0000000..78272a2 --- /dev/null +++ b/src/main/java/carpet/logging/logHelpers/TNTLogHelper.java @@ -0,0 +1,51 @@ +package carpet.logging.logHelpers; + +import carpet.logging.LoggerRegistry; +import carpet.utils.Messenger; +import net.minecraft.network.chat.Component; +import net.minecraft.world.phys.Vec3; + +public class TNTLogHelper +{ + public boolean initialized; + private double primedX, primedY, primedZ; + private static long lastGametime = 0; + private static int tntCount = 0; + private Vec3 primedAngle; + /** + * Runs when the TNT is primed. Expects the position and motion angle of the TNT. + */ + public void onPrimed(double x, double y, double z, Vec3 motion) + { + primedX = x; + primedY = y; + primedZ = z; + primedAngle = motion; + initialized = true; + } + /** + * Runs when the TNT explodes. Expects the position of the TNT. + */ + 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) { + case "brief" -> new Component[]{Messenger.c( + "l P ", Messenger.dblt("l", primedX, primedY, primedZ), + "w ", Messenger.dblt("l", primedAngle.x, primedAngle.y, primedAngle.z), + "r E ", Messenger.dblt("r", x, y, z))}; + case "full" -> new Component[]{Messenger.c( + "r #" + tntCount, + "m @" + 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))}; + default -> null; + }); + } +} diff --git a/src/main/java/carpet/logging/logHelpers/TrajectoryLogHelper.java b/src/main/java/carpet/logging/logHelpers/TrajectoryLogHelper.java new file mode 100644 index 0000000..064510d --- /dev/null +++ b/src/main/java/carpet/logging/logHelpers/TrajectoryLogHelper.java @@ -0,0 +1,75 @@ +package carpet.logging.logHelpers; + +import carpet.logging.Logger; +import carpet.logging.LoggerRegistry; +import carpet.utils.Messenger; +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.network.chat.Component; +import net.minecraft.world.phys.Vec3; + +/** + * A generic log helper for logging the trajectory of things like blocks and throwables. + */ +public class TrajectoryLogHelper +{ + private static final int MAX_TICKS_PER_LINE = 20; + + private boolean doLog; + private final Logger logger; + + private final ArrayList positions = new ArrayList<>(); + private final ArrayList motions = new ArrayList<>(); + + public TrajectoryLogHelper(String logName) + { + this.logger = LoggerRegistry.getLogger(logName); + this.doLog = this.logger.hasOnlineSubscribers(); + } + + public void onTick(double x, double y, double z, Vec3 velocity) + { + if (!doLog) return; + positions.add(new Vec3(x, y, z)); + motions.add(velocity); + } + + public void onFinish() + { + if (!doLog) return; + logger.log( (option) -> { + List comp = new ArrayList<>(); + switch (option) { + case "brief" -> { + comp.add(Messenger.s("")); + List line = new ArrayList<>(); + for (int i = 0; i < positions.size(); i++) { + Vec3 pos = positions.get(i); + Vec3 mot = motions.get(i); + line.add("w x"); + line.add(String.format("^w Tick: %d\nx: %f\ny: %f\nz: %f\n------------\nmx: %f\nmy: %f\nmz: %f", + i, pos.x, pos.y, pos.z, mot.x, mot.y, mot.z)); + if ((((i + 1) % MAX_TICKS_PER_LINE) == 0) || i == positions.size() - 1) { + comp.add(Messenger.c(line.toArray(new Object[0]))); + line.clear(); + } + } + } + case "full" -> { + comp.add(Messenger.c("w ---------")); + for (int i = 0; i < positions.size(); i++) { + Vec3 pos = positions.get(i); + Vec3 mot = motions.get(i); + comp.add(Messenger.c( + String.format("w tick: %3d pos", i), Messenger.dblt("w", pos.x, pos.y, pos.z), + "w mot", Messenger.dblt("w", mot.x, mot.y, mot.z))); + } + } + } + return comp.toArray(new Component[0]); + }); + doLog = false; + } +} + diff --git a/src/main/java/carpet/mixins/AbstractArrowMixin.java b/src/main/java/carpet/mixins/AbstractArrowMixin.java new file mode 100644 index 0000000..e300859 --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractArrowMixin.java @@ -0,0 +1,57 @@ +package carpet.mixins; + +import carpet.logging.LoggerRegistry; +import carpet.logging.logHelpers.TrajectoryLogHelper; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.projectile.AbstractArrow; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +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(AbstractArrow.class) +public abstract class AbstractArrowMixin extends Entity +{ + private TrajectoryLogHelper logHelper; + public AbstractArrowMixin(EntityType entityType_1, Level world_1) { super(entityType_1, world_1); } + + @Inject(method = "(Lnet/minecraft/world/entity/EntityType;DDDLnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/item/ItemStack;)V", at = @At("RETURN")) + private void addLogger(final EntityType entityType, final double x, final double y, final double z, final Level level, final ItemStack itemStack, final ItemStack weapon, final CallbackInfo ci) + { + if (LoggerRegistry.__projectiles && !level.isClientSide) + logHelper = new TrajectoryLogHelper("projectiles"); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void tickCheck(CallbackInfo ci) + { + if (LoggerRegistry.__projectiles && logHelper != null) + logHelper.onTick(getX(), getY(), getZ(), getDeltaMovement()); + } + + // todo should be moved on one place this is acceessed from + @Inject(method = "onHitEntity", at = @At("RETURN")) + private void removeOnEntity(EntityHitResult entityHitResult, CallbackInfo ci) + { + if (LoggerRegistry.__projectiles && logHelper != null) + { + logHelper.onFinish(); + logHelper = null; + } + } + + @Inject(method = "onHitBlock", at = @At("RETURN")) + private void removeOnBlock(BlockHitResult blockHitResult, CallbackInfo ci) + { + if (LoggerRegistry.__projectiles && logHelper != null) + { + logHelper.onFinish(); + logHelper = null; + } + } +} diff --git a/src/main/java/carpet/mixins/AbstractCauldronBlock_stackableSBoxesMixin.java b/src/main/java/carpet/mixins/AbstractCauldronBlock_stackableSBoxesMixin.java new file mode 100644 index 0000000..95cc0db --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractCauldronBlock_stackableSBoxesMixin.java @@ -0,0 +1,40 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.core.cauldron.CauldronInteraction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.AbstractCauldronBlock; +import net.minecraft.world.level.block.ShulkerBoxBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(AbstractCauldronBlock.class) +public class AbstractCauldronBlock_stackableSBoxesMixin +{ + @Redirect(method = "useItemOn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/core/cauldron/CauldronInteraction;interact(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/item/ItemStack;)Lnet/minecraft/world/ItemInteractionResult;" + )) + private ItemInteractionResult wrapInteractor(final CauldronInteraction cauldronBehavior, final BlockState blockState, final Level world, final BlockPos blockPos, final Player playerEntity, final InteractionHand hand, final ItemStack itemStack) + { + int count = -1; + if (CarpetSettings.shulkerBoxStackSize > 1 && itemStack.getItem() instanceof BlockItem && ((BlockItem)itemStack.getItem()).getBlock() instanceof ShulkerBoxBlock) + count = itemStack.getCount(); + ItemInteractionResult result = cauldronBehavior.interact(blockState, world, blockPos, playerEntity, hand, itemStack); + if (count > 0 && result.consumesAction()) + { + ItemStack current = playerEntity.getItemInHand(hand); + if (current.getItem() instanceof BlockItem && ((BlockItem)itemStack.getItem()).getBlock() instanceof ShulkerBoxBlock) + current.setCount(count); + } + return result; + } +} diff --git a/src/main/java/carpet/mixins/AbstractContainerMenuSubclasses_scarpetMixin.java b/src/main/java/carpet/mixins/AbstractContainerMenuSubclasses_scarpetMixin.java new file mode 100644 index 0000000..1b72742 --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractContainerMenuSubclasses_scarpetMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.fakes.AbstractContainerMenuInterface; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.EnchantmentMenu; +import net.minecraft.world.inventory.LecternMenu; +import net.minecraft.world.inventory.LoomMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.StonecutterMenu; +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; + + +//classes that override onButtonClick +@Mixin({EnchantmentMenu.class, LecternMenu.class, LoomMenu.class, StonecutterMenu.class}) +public abstract class AbstractContainerMenuSubclasses_scarpetMixin extends AbstractContainerMenu { + protected AbstractContainerMenuSubclasses_scarpetMixin(MenuType type, int syncId) { + super(type, syncId); + } + + @Inject(method = "clickMenuButton", at = @At("HEAD"), cancellable = true) + private void buttonClickCallback(Player player, int id, CallbackInfoReturnable cir) { + if(((AbstractContainerMenuInterface) this).callButtonClickListener(id,player)) + cir.cancel(); + } +} diff --git a/src/main/java/carpet/mixins/AbstractContainerMenu_ctrlQCraftingMixin.java b/src/main/java/carpet/mixins/AbstractContainerMenu_ctrlQCraftingMixin.java new file mode 100644 index 0000000..49620f2 --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractContainerMenu_ctrlQCraftingMixin.java @@ -0,0 +1,55 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +@Mixin(AbstractContainerMenu.class) +public abstract class AbstractContainerMenu_ctrlQCraftingMixin +{ + + @Shadow public abstract void clicked(int int_1, int int_2, ClickType slotActionType_1, Player playerEntity_1); + + @Shadow public abstract void broadcastChanges(); + + @Shadow @Final public NonNullList slots; + + @Shadow protected abstract void resetQuickCraft(); + + @Shadow public abstract ItemStack getCarried(); + + @Inject( method = "doClick", at = @At(value = "HEAD"), cancellable = true) + private void onThrowClick(int slotId, int clickData, ClickType actionType, Player playerEntity, CallbackInfo ci) + { + if (actionType == ClickType.THROW && CarpetSettings.ctrlQCraftingFix && this.getCarried().isEmpty() && slotId >= 0) + { + Slot slot_4 = slots.get(slotId); + if (/*slot_4 != null && */slot_4.hasItem() && slot_4.mayPickup(playerEntity)) + { + if(slotId == 0 && clickData == 1) + { + Item craftedItem = slot_4.getItem().getItem(); + while(slot_4.hasItem() && slot_4.getItem().getItem() == craftedItem) + { + this.clicked(slotId, 0, ClickType.THROW, playerEntity); + } + this.broadcastChanges(); + this.resetQuickCraft(); + ci.cancel(); + } + } + } + } +} diff --git a/src/main/java/carpet/mixins/AbstractContainerMenu_scarpetMixin.java b/src/main/java/carpet/mixins/AbstractContainerMenu_scarpetMixin.java new file mode 100644 index 0000000..a1a55ee --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractContainerMenu_scarpetMixin.java @@ -0,0 +1,78 @@ +package carpet.mixins; + +import carpet.fakes.AbstractContainerMenuInterface; +import carpet.script.value.ScreenValue; +import net.minecraft.world.item.crafting.RecipeHolder; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.inventory.DataSlot; + +@Mixin(AbstractContainerMenu.class) +public abstract class AbstractContainerMenu_scarpetMixin implements AbstractContainerMenuInterface +{ + @Shadow @Final private List containerListeners; + @Shadow public abstract void sendAllDataToRemote(); + @Shadow @Final private List dataSlots; + + @Inject(method = "doClick", at = @At("HEAD"), cancellable = true) + private void callSlotClickListener(int slotIndex, int button, ClickType actionType, Player player, CallbackInfo ci) { + if(!(player instanceof ServerPlayer serverPlayerEntity)) return; + for(ContainerListener screenHandlerListener : this.containerListeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + if(scarpetScreenHandlerListener.onSlotClick(serverPlayerEntity, actionType, slotIndex, button)) { + ci.cancel(); + sendAllDataToRemote(); + } + } + } + } + + @Inject(method = "removed", at = @At("HEAD"), cancellable = true) + private void callCloseListener(Player player, CallbackInfo ci) { + if(!(player instanceof ServerPlayer serverPlayerEntity)) return; + for(ContainerListener screenHandlerListener : this.containerListeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + scarpetScreenHandlerListener.onClose(serverPlayerEntity); + } + } + } + + @Override + public boolean callButtonClickListener(int button, Player player) { + if(!(player instanceof ServerPlayer serverPlayerEntity)) return false; + for(ContainerListener screenHandlerListener : containerListeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + if(scarpetScreenHandlerListener.onButtonClick(serverPlayerEntity, button)) + return true; + } + } + return false; + } + + @Override + public boolean callSelectRecipeListener(ServerPlayer player, RecipeHolder recipe, boolean craftAll) { + for(ContainerListener screenHandlerListener : containerListeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + if(scarpetScreenHandlerListener.onSelectRecipe(player, recipe, craftAll)) + return true; + } + } + return false; + } + + @Override + public DataSlot getDataSlot(int index) { + return this.dataSlots.get(index); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/AbstractMinecart_scarpetEventsMixin.java b/src/main/java/carpet/mixins/AbstractMinecart_scarpetEventsMixin.java new file mode 100644 index 0000000..dd9a16b --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractMinecart_scarpetEventsMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.level.Level; +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(AbstractMinecart.class) +public abstract class AbstractMinecart_scarpetEventsMixin extends Entity +{ + public AbstractMinecart_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + // calling extra on_tick because falling blocks do not fall back to super tick call + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_TICK); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/ArmorStand_scarpetMarkerMixin.java b/src/main/java/carpet/mixins/ArmorStand_scarpetMarkerMixin.java new file mode 100644 index 0000000..1a94430 --- /dev/null +++ b/src/main/java/carpet/mixins/ArmorStand_scarpetMarkerMixin.java @@ -0,0 +1,54 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import carpet.script.api.Auxiliary; +import org.apache.commons.lang3.StringUtils; +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; + +import java.util.Optional; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.level.Level; + +@Mixin(ArmorStand.class) +public abstract class ArmorStand_scarpetMarkerMixin extends LivingEntity +{ + protected ArmorStand_scarpetMarkerMixin(EntityType entityType_1, Level world_1) + { + super(entityType_1, world_1); + } + + /** + * Remove all markers that do not belong to any script host and not part of the global one when loaded + * @param ci + */ + @Inject(method = "readAdditionalSaveData", at = @At("HEAD")) + private void checkScarpetMarkerUnloaded(CallbackInfo ci) + { + if (!level().isClientSide) + { + if (getTags().contains(Auxiliary.MARKER_STRING)) + { + String prefix = Auxiliary.MARKER_STRING+"_"; + Optional owner = getTags().stream().filter(s -> s.startsWith(prefix)).findFirst(); + if (owner.isPresent()) + { + String hostName = StringUtils.removeStart(owner.get(),prefix); + if (!hostName.isEmpty() && CarpetServer.scriptServer.getAppHostByName(hostName) == null) + { + discard(); //discard + } + + } + else + { + discard(); // discard + } + } + } + } +} diff --git a/src/main/java/carpet/mixins/BarrierBlock_updateSuppressionBlockMixin.java b/src/main/java/carpet/mixins/BarrierBlock_updateSuppressionBlockMixin.java new file mode 100644 index 0000000..a4f2cc5 --- /dev/null +++ b/src/main/java/carpet/mixins/BarrierBlock_updateSuppressionBlockMixin.java @@ -0,0 +1,57 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.LevelInterface; +import net.minecraft.world.level.redstone.NeighborUpdater; +import net.minecraft.util.RandomSource; +import org.spongepowered.asm.mixin.Mixin; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BarrierBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.PoweredRailBlock; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(BarrierBlock.class) +public class BarrierBlock_updateSuppressionBlockMixin extends Block { + private boolean shouldPower = false; + + public BarrierBlock_updateSuppressionBlockMixin(Properties settings) { super(settings); } + + @Override + public int getSignal(BlockState state, BlockGetter level, BlockPos pos, Direction direction) { + return (shouldPower && direction == Direction.DOWN) ? 15 : 0; + } + + @Override + public void neighborChanged(BlockState state, Level level, BlockPos pos, Block block, BlockPos fromPos, boolean notify) { + if (CarpetSettings.updateSuppressionBlock != -1) { + if (fromPos.equals(pos.above())) { + BlockState stateAbove = level.getBlockState(fromPos); + if (stateAbove.is(Blocks.ACTIVATOR_RAIL) && !stateAbove.getValue(PoweredRailBlock.POWERED)) { + level.scheduleTick(pos, this, 1); + NeighborUpdater updater = ((LevelInterface)level).getNeighborUpdater(); + if (updater instanceof CollectingNeighborUpdaterAccessor cnua) + cnua.setCount(cnua.getMaxChainedNeighborUpdates()-CarpetSettings.updateSuppressionBlock); + } + } + } + super.neighborChanged(state, level, pos, block, fromPos, notify); + } + + @Override + public void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + BlockPos posAbove = pos.above(); + BlockState stateAbove = level.getBlockState(posAbove); + if (stateAbove.is(Blocks.ACTIVATOR_RAIL) && !stateAbove.getValue(PoweredRailBlock.POWERED)) { + shouldPower = true; + level.setBlock(posAbove, stateAbove.setValue(PoweredRailBlock.POWERED, true), Block.UPDATE_CLIENTS | Block.UPDATE_NONE); + shouldPower = false; + } + } +} diff --git a/src/main/java/carpet/mixins/Biome_scarpetMixin.java b/src/main/java/carpet/mixins/Biome_scarpetMixin.java new file mode 100644 index 0000000..496c2bd --- /dev/null +++ b/src/main/java/carpet/mixins/Biome_scarpetMixin.java @@ -0,0 +1,17 @@ +package carpet.mixins; + +import carpet.fakes.BiomeInterface; +import net.minecraft.world.level.biome.Biome; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(Biome.class) +public class Biome_scarpetMixin implements BiomeInterface { + @Shadow + private Biome.ClimateSettings climateSettings; + + @Override + public Biome.ClimateSettings getClimateSettings() { + return climateSettings; + } +} diff --git a/src/main/java/carpet/mixins/BlockBehaviourBlockStateBase_mixin.java b/src/main/java/carpet/mixins/BlockBehaviourBlockStateBase_mixin.java new file mode 100644 index 0000000..3e512d9 --- /dev/null +++ b/src/main/java/carpet/mixins/BlockBehaviourBlockStateBase_mixin.java @@ -0,0 +1,27 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.BuddingAmethystBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.PushReaction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockBehaviour.BlockStateBase.class) +public abstract class BlockBehaviourBlockStateBase_mixin +{ + @Shadow public abstract Block getBlock(); + + @Inject(method = "getPistonPushReaction", at = @At("HEAD"), cancellable = true) + private void onGetPistonPushReaction(CallbackInfoReturnable cir) + { + if (CarpetSettings.movableAmethyst && getBlock() instanceof BuddingAmethystBlock) + { + cir.setReturnValue(PushReaction.NORMAL); + } + } +} diff --git a/src/main/java/carpet/mixins/BlockEntity_movableBEMixin.java b/src/main/java/carpet/mixins/BlockEntity_movableBEMixin.java new file mode 100644 index 0000000..a043624 --- /dev/null +++ b/src/main/java/carpet/mixins/BlockEntity_movableBEMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.fakes.BlockEntityInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(BlockEntity.class) +public abstract class BlockEntity_movableBEMixin implements BlockEntityInterface +{ + @Mutable + @Shadow @Final protected BlockPos worldPosition; + + public void setCMPos(BlockPos newPos) + { + worldPosition = newPos; + }; +} diff --git a/src/main/java/carpet/mixins/BlockInput_fillUpdatesMixin.java b/src/main/java/carpet/mixins/BlockInput_fillUpdatesMixin.java new file mode 100644 index 0000000..c06bff0 --- /dev/null +++ b/src/main/java/carpet/mixins/BlockInput_fillUpdatesMixin.java @@ -0,0 +1,30 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import carpet.CarpetSettings; +import net.minecraft.commands.arguments.blocks.BlockInput; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(BlockInput.class) +public class BlockInput_fillUpdatesMixin +{ + @Redirect(method = "place", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;updateFromNeighbourShapes(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/LevelAccessor;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;" + )) + private BlockState postProcessStateProxy(BlockState state, LevelAccessor serverWorld, BlockPos blockPos) + { + if (CarpetSettings.impendingFillSkipUpdates.get()) + { + return state; + } + + return Block.updateFromNeighbourShapes(state, serverWorld, blockPos); + } +} diff --git a/src/main/java/carpet/mixins/BlockInput_scarpetMixin.java b/src/main/java/carpet/mixins/BlockInput_scarpetMixin.java new file mode 100644 index 0000000..44261a6 --- /dev/null +++ b/src/main/java/carpet/mixins/BlockInput_scarpetMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.fakes.BlockStateArgumentInterface; +import net.minecraft.commands.arguments.blocks.BlockInput; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(BlockInput.class) +public class BlockInput_scarpetMixin implements BlockStateArgumentInterface +{ + @Shadow @Final private @Nullable CompoundTag tag; + + @Override + public CompoundTag getCMTag() + { + return tag; + } +} 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 0000000..a89577f --- /dev/null +++ b/src/main/java/carpet/mixins/BlockItem_creativeNoClipMixin.java @@ -0,0 +1,36 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +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/level/Level;isUnobstructed(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/phys/shapes/CollisionContext;)Z" + )) + private boolean canSpectatingPlace(Level world, BlockState state, BlockPos pos, CollisionContext context, + BlockPlaceContext contextOuter, BlockState stateOuter) + { + Player player = contextOuter.getPlayer(); + if (CarpetSettings.creativeNoClip && player != null && player.isCreative() && player.getAbilities().flying) + { + // copy from canPlace + VoxelShape voxelShape = state.getCollisionShape(world, pos, context); + return voxelShape.isEmpty() || world.isUnobstructed(player, voxelShape.move(pos.getX(), pos.getY(), pos.getZ())); + + } + return world.isUnobstructed(state, pos, context); + } +} diff --git a/src/main/java/carpet/mixins/BlockItem_scarpetEventMixin.java b/src/main/java/carpet/mixins/BlockItem_scarpetEventMixin.java new file mode 100644 index 0000000..2a2d43b --- /dev/null +++ b/src/main/java/carpet/mixins/BlockItem_scarpetEventMixin.java @@ -0,0 +1,40 @@ +package carpet.mixins; + +import net.minecraft.world.level.block.state.BlockState; +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_PLACES_BLOCK; +import static carpet.script.CarpetEventServer.Event.PLAYER_PLACING_BLOCK; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; + +@Mixin(BlockItem.class) +public class BlockItem_scarpetEventMixin +{ + @Inject(method = "place(Lnet/minecraft/world/item/context/BlockPlaceContext;)Lnet/minecraft/world/InteractionResult;", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;setPlacedBy(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/item/ItemStack;)V", + shift = At.Shift.AFTER + )) + private void afterPlacement(BlockPlaceContext context, CallbackInfoReturnable cir) + { + if (context.getPlayer() instanceof ServerPlayer && PLAYER_PLACES_BLOCK.isNeeded()) + PLAYER_PLACES_BLOCK.onBlockPlaced((ServerPlayer) context.getPlayer(), context.getClickedPos(), context.getHand(), context.getItemInHand()); + } + + @Inject(method = "placeBlock", at = @At("HEAD"), cancellable = true) + private void beforePlacement(BlockPlaceContext context, BlockState placementState, CallbackInfoReturnable cir) { + if (context.getPlayer() instanceof ServerPlayer && PLAYER_PLACING_BLOCK.isNeeded()) { + if (PLAYER_PLACING_BLOCK.onBlockPlaced((ServerPlayer) context.getPlayer(), context.getClickedPos(), context.getHand(), context.getItemInHand())) { + cir.setReturnValue(false); + cir.cancel(); + } + } + } +} diff --git a/src/main/java/carpet/mixins/BlockPredicate_scarpetMixin.java b/src/main/java/carpet/mixins/BlockPredicate_scarpetMixin.java new file mode 100644 index 0000000..ed4c588 --- /dev/null +++ b/src/main/java/carpet/mixins/BlockPredicate_scarpetMixin.java @@ -0,0 +1,57 @@ +package carpet.mixins; + +import carpet.fakes.BlockPredicateInterface; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import net.minecraft.tags.TagKey; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; + +@Mixin(targets = "net.minecraft.commands.arguments.blocks.BlockPredicateArgument$BlockPredicate") +public class BlockPredicate_scarpetMixin implements BlockPredicateInterface +{ + + @Shadow @Final private BlockState state; + + @Shadow @Final /*@Nullable*/ private CompoundTag nbt; + + @Shadow @Final private Set> properties; + + @Override + public BlockState getCMBlockState() + { + return state; + } + + @Override + public TagKey getCMBlockTagKey() + { + return null; + } + + @Override + public Map getCMProperties() + { + return properties.stream().collect(Collectors.toMap( + p -> StringValue.of(p.getName()), + p -> ValueConversions.fromProperty(state, p), + (a, b) -> b + )); + } + + @Override + public CompoundTag getCMDataTag() + { + return nbt; + } +} diff --git a/src/main/java/carpet/mixins/BoundTickingBlockEntity_profilerMixin.java b/src/main/java/carpet/mixins/BoundTickingBlockEntity_profilerMixin.java new file mode 100644 index 0000000..a680d28 --- /dev/null +++ b/src/main/java/carpet/mixins/BoundTickingBlockEntity_profilerMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.utils.CarpetProfiler; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets = "net.minecraft.world.level.chunk.LevelChunk$BoundTickingBlockEntity") +public class BoundTickingBlockEntity_profilerMixin +{ + @Shadow @Final private T blockEntity; + CarpetProfiler.ProfilerToken entitySection; + + @Inject(method = "tick()V", at = @At("HEAD")) + private void startTileEntitySection(CallbackInfo ci) + { + entitySection = CarpetProfiler.start_block_entity_section(blockEntity.getLevel(), blockEntity, CarpetProfiler.TYPE.TILEENTITY); + } + + @Inject(method = "tick()V", at = @At("RETURN")) + private void endTileEntitySection(CallbackInfo ci) + { + CarpetProfiler.end_current_entity_section(entitySection); + } +} diff --git a/src/main/java/carpet/mixins/BuddingAmethystBlock_movableAmethystMixin.java b/src/main/java/carpet/mixins/BuddingAmethystBlock_movableAmethystMixin.java new file mode 100644 index 0000000..e18b5e5 --- /dev/null +++ b/src/main/java/carpet/mixins/BuddingAmethystBlock_movableAmethystMixin.java @@ -0,0 +1,41 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.PickaxeItem; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.BuddingAmethystBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.PushReaction; +import org.jetbrains.annotations.Nullable; +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(BuddingAmethystBlock.class) +public class BuddingAmethystBlock_movableAmethystMixin extends Block { + public BuddingAmethystBlock_movableAmethystMixin(Properties settings) { + super(settings); + } + + @Override + public void playerDestroy(Level world, Player player, BlockPos pos, BlockState state, @Nullable BlockEntity blockEntity, ItemStack stack) { + super.playerDestroy(world, player, pos, state, blockEntity, stack); + // doing it here rather than though loottables since loottables are loaded on reload + // drawback - not controlled via loottables, but hey + if (CarpetSettings.movableAmethyst && + stack.getItem() instanceof PickaxeItem && + EnchantmentHelper.getItemEnchantmentLevel(world.registryAccess().registryOrThrow(Registries.ENCHANTMENT).getHolderOrThrow(Enchantments.SILK_TOUCH), stack) > 0 + ) + popResource(world, pos, Items.BUDDING_AMETHYST.getDefaultInstance()); + } +} diff --git a/src/main/java/carpet/mixins/ChainBlock_customStickyMixin.java b/src/main/java/carpet/mixins/ChainBlock_customStickyMixin.java new file mode 100644 index 0000000..c7e3d72 --- /dev/null +++ b/src/main/java/carpet/mixins/ChainBlock_customStickyMixin.java @@ -0,0 +1,47 @@ +package carpet.mixins; + +import carpet.fakes.BlockPistonBehaviourInterface; +import org.spongepowered.asm.mixin.Mixin; + +import carpet.CarpetSettings; +import carpet.CarpetSettings.ChainStoneMode; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Direction.Axis; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.ChainBlock; +import net.minecraft.world.level.block.EndRodBlock; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(ChainBlock.class) +public class ChainBlock_customStickyMixin implements BlockPistonBehaviourInterface { + + @Override + public boolean isSticky(BlockState state) { + return CarpetSettings.chainStone.enabled(); + } + + @Override + public boolean isStickyToNeighbor(Level level, BlockPos pos, BlockState state, BlockPos neighborPos, BlockState neighborState, Direction dir, Direction moveDir) { + Axis axis = state.getValue(ChainBlock.AXIS); + + if (axis != dir.getAxis()) { + return false; + } + + if (CarpetSettings.chainStone == ChainStoneMode.STICK_TO_ALL) { + return true; + } + if (neighborState.is((Block)(Object)this)) { + return axis == neighborState.getValue(ChainBlock.AXIS); + } + if (neighborState.is(Blocks.END_ROD)) { + return axis == neighborState.getValue(EndRodBlock.FACING).getAxis(); + } + + return Block.canSupportCenter(level, neighborPos, dir.getOpposite()); + } +} diff --git a/src/main/java/carpet/mixins/ChestBlock_customStickyMixin.java b/src/main/java/carpet/mixins/ChestBlock_customStickyMixin.java new file mode 100644 index 0000000..b314836 --- /dev/null +++ b/src/main/java/carpet/mixins/ChestBlock_customStickyMixin.java @@ -0,0 +1,41 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Mixin; + +import carpet.CarpetSettings; +import carpet.fakes.BlockPistonBehaviourInterface; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.ChestType; + +import static net.minecraft.world.level.block.ChestBlock.getConnectedDirection; + +@Mixin(ChestBlock.class) +public class ChestBlock_customStickyMixin implements BlockPistonBehaviourInterface { + + @Override + public boolean isSticky(BlockState state) { + return CarpetSettings.movableBlockEntities; + } + + @Override + public boolean isStickyToNeighbor(Level level, BlockPos pos, BlockState state, BlockPos neighborPos, BlockState neighborState, Direction dir, Direction moveDir) { + if (!neighborState.is((Block)(Object)this)) { + return false; + } + + ChestType type = state.getValue(ChestBlock.TYPE); + ChestType neighborType = neighborState.getValue(ChestBlock.TYPE); + + if (type == ChestType.SINGLE || neighborType == ChestType.SINGLE) { + return false; + } + + return getConnectedDirection(state) == dir; + } +} diff --git a/src/main/java/carpet/mixins/ChunkAccess_warningsMixin.java b/src/main/java/carpet/mixins/ChunkAccess_warningsMixin.java new file mode 100644 index 0000000..2be7d8e --- /dev/null +++ b/src/main/java/carpet/mixins/ChunkAccess_warningsMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.chunk.ChunkAccess; +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(ChunkAccess.class) +public abstract class ChunkAccess_warningsMixin +{ + // failed to mixin into interface. need to mixin in two places that uses it + @Inject(method = "markPosForPostprocessing(Lnet/minecraft/core/BlockPos;)V", + at =@At("HEAD"), cancellable = true) + private void squashWarnings(BlockPos blockPos_1, CallbackInfo ci) + { + if (CarpetSettings.skipGenerationChecks.get()) ci.cancel(); + } +} diff --git a/src/main/java/carpet/mixins/ChunkGenerator_customMobSpawnsMixin.java b/src/main/java/carpet/mixins/ChunkGenerator_customMobSpawnsMixin.java new file mode 100644 index 0000000..0686817 --- /dev/null +++ b/src/main/java/carpet/mixins/ChunkGenerator_customMobSpawnsMixin.java @@ -0,0 +1,43 @@ +package carpet.mixins; + +import carpet.utils.SpawnOverrides; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.Holder; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.levelgen.structure.Structure; +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 net.minecraft.core.BlockPos; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.chunk.ChunkGenerator; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.Iterator; +import java.util.Map; + +@Mixin(ChunkGenerator.class) +public abstract class ChunkGenerator_customMobSpawnsMixin +{ + @Inject( + method = "getMobsAt", locals = LocalCapture.CAPTURE_FAILHARD, + at = @At( + value = "INVOKE", + target = "Ljava/util/Map$Entry;getKey()Ljava/lang/Object;" + ), cancellable = true) + private void checkCMSpawns(Holder holder, StructureManager structureFeatureManager, MobCategory mobCategory, BlockPos blockPos, + CallbackInfoReturnable> cir, + Map map, Iterator var6, Map.Entry entry) + { + WeightedRandomList res = SpawnOverrides.test(structureFeatureManager, entry.getValue(), mobCategory, entry.getKey(), blockPos); + if (res != null) + { + cir.setReturnValue(res); + } + } +} diff --git a/src/main/java/carpet/mixins/ChunkHolder_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/ChunkHolder_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..1727b83 --- /dev/null +++ b/src/main/java/carpet/mixins/ChunkHolder_scarpetChunkCreationMixin.java @@ -0,0 +1,42 @@ +package carpet.mixins; + +import carpet.fakes.ChunkHolderInterface; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReferenceArray; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.UpgradeData; + +@Mixin(ChunkHolder.class) +public abstract class ChunkHolder_scarpetChunkCreationMixin implements ChunkHolderInterface +{ + //@Shadow protected abstract void updateChunkToSave(CompletableFuture> newChunkFuture, String type); + + //@Shadow @Final private AtomicReferenceArray>> futures; + + /* + @Override + public CompletableFuture> setDefaultProtoChunk(ChunkPos chpos, BlockableEventLoop executor, ServerLevel world) + { + int i = ChunkStatus.EMPTY.getIndex(); + CompletableFuture> completableFuture2 = CompletableFuture.supplyAsync( + () -> ChunkResult.of(new ProtoChunk(chpos, UpgradeData.EMPTY, world, world.registryAccess().registryOrThrow(Registries.BIOME), null)), // todo figure out what that does - maybe add an option to reset with blending enabled..? + executor + ); + updateChunkToSave(completableFuture2, "unfull"); // possible debug data + futures.set(i, completableFuture2); + return completableFuture2; + } + */ +} diff --git a/src/main/java/carpet/mixins/ChunkMap_creativePlayersLoadChunksMixin.java b/src/main/java/carpet/mixins/ChunkMap_creativePlayersLoadChunksMixin.java new file mode 100644 index 0000000..1f046bf --- /dev/null +++ b/src/main/java/carpet/mixins/ChunkMap_creativePlayersLoadChunksMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerPlayer; +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(ChunkMap.class) +public class ChunkMap_creativePlayersLoadChunksMixin { + + @Inject(method = "skipPlayer(Lnet/minecraft/server/level/ServerPlayer;)Z", at = @At("HEAD"), cancellable = true) + private void startProfilerSection(ServerPlayer serverPlayer, CallbackInfoReturnable cir) + { + if (!CarpetSettings.creativePlayersLoadChunks && serverPlayer.isCreative()) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/carpet/mixins/ChunkMap_profilerMixin.java b/src/main/java/carpet/mixins/ChunkMap_profilerMixin.java new file mode 100644 index 0000000..3b2235c --- /dev/null +++ b/src/main/java/carpet/mixins/ChunkMap_profilerMixin.java @@ -0,0 +1,35 @@ +package carpet.mixins; + +import carpet.utils.CarpetProfiler; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.BooleanSupplier; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; + +@Mixin(ChunkMap.class) +public class ChunkMap_profilerMixin +{ + @Shadow @Final ServerLevel level; + CarpetProfiler.ProfilerToken currentSection; + + @Inject(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At("HEAD")) + private void startProfilerSection(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(level, "Unloading", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At("RETURN")) + private void stopProfilerSecion(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + } + } +} diff --git a/src/main/java/carpet/mixins/ChunkMap_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/ChunkMap_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..fc300db --- /dev/null +++ b/src/main/java/carpet/mixins/ChunkMap_scarpetChunkCreationMixin.java @@ -0,0 +1,569 @@ +package carpet.mixins; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.function.IntFunction; +import java.util.stream.Collectors; + +import carpet.fakes.SimpleEntityLookupInterface; +import carpet.fakes.ServerWorldInterface; +import net.minecraft.Util; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkLevel; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ChunkMap.DistanceManager; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ChunkTaskPriorityQueueSorter; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.server.level.TicketType; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.util.thread.BlockableEventLoop; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.RegionFile; +import org.apache.commons.lang3.tuple.Pair; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import carpet.fakes.ChunkHolderInterface; +import carpet.fakes.ChunkTicketManagerInterface; +import carpet.fakes.ServerLightingProviderInterface; +import carpet.fakes.ThreadedAnvilChunkStorageInterface; +import carpet.script.utils.WorldTools; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +import static carpet.script.CarpetEventServer.Event.CHUNK_GENERATED; +import static carpet.script.CarpetEventServer.Event.CHUNK_LOADED; + +@Mixin(ChunkMap.class) +public abstract class ChunkMap_scarpetChunkCreationMixin implements ThreadedAnvilChunkStorageInterface +{ + @Shadow + @Final + private ServerLevel level; + + @Shadow + @Final + private Long2ObjectLinkedOpenHashMap updatingChunkMap; + + @Shadow + private boolean modified; + + @Shadow + @Final + private ThreadedLevelLightEngine lightEngine; + + @Shadow + @Final + private ChunkTaskPriorityQueueSorter queueSorter; + + @Shadow + @Final + private BlockableEventLoop mainThreadExecutor; + + @Shadow + @Final + private ChunkProgressListener progressListener; + + @Shadow + @Final + private DistanceManager distanceManager; + + @Shadow + protected abstract boolean promoteChunkMap(); + + @Shadow + protected abstract Iterable getChunks(); + + + @Shadow + protected abstract CompletableFuture>> getChunkRangeFuture(ChunkHolder chunkHolder, int i, IntFunction intFunction); + + //@Shadow protected abstract void postLoadProtoChunk(final ServerLevel serverLevel, final List list); + + ThreadLocal generated = ThreadLocal.withInitial(() -> null); + + // in protoChunkToFullChunk + // fancier version of the one below, ensuring that the event is triggered when the chunk is actually loaded. + + /* + + + @Inject(method = "method_17227", at = @At("HEAD"), remap = false) + private void onChunkGeneratedStart(ChunkHolder chunkHolder, ChunkAccess chunkAccess, CallbackInfoReturnable cir) + { + if (CHUNK_GENERATED.isNeeded() || CHUNK_LOADED.isNeeded()) + { + generated.set(chunkHolder.getLastAvailable().getStatus() != ChunkStatus.FULL); + } + else + { + generated.set(null); + } + } + + @Inject(method = "method_17227", at = @At("RETURN"), remap = false) + private void onChunkGeneratedEnd(ChunkHolder chunkHolder, ChunkAccess chunk, CallbackInfoReturnable>> cir) + { + Boolean localGenerated = generated.get(); + if (localGenerated != null) + { + MinecraftServer server = this.level.getServer(); + int ticks = server.getTickCount(); + ChunkPos chpos = chunkHolder.getPos(); + // need to send these because if an app does something with that event, it may lock the thread + // so better be safe and schedule it for later, aSaP + if (CHUNK_GENERATED.isNeeded() && localGenerated) + { + server.tell(new TickTask(ticks, () -> CHUNK_GENERATED.onChunkEvent(this.level, chpos, true))); + } + if (CHUNK_LOADED.isNeeded()) + { + server.tell(new TickTask(ticks, () -> CHUNK_LOADED.onChunkEvent(this.level, chpos, localGenerated))); + } + } + } + + */ + + /* simple but a version that doesn't guarantee that the chunk is actually loaded + @Inject(method = "convertToFullChunk", at = @At("HEAD")) + private void onChunkGeneratedEnd(ChunkHolder chunkHolder, CallbackInfoReturnable>> cir) + { + if (CHUNK_GENERATED.isNeeded() && chunkHolder.getCurrentChunk().getStatus() != ChunkStatus.FULL) + { + ChunkPos chpos = chunkHolder.getPos(); + this.world.getServer().execute(() -> CHUNK_GENERATED.onChunkEvent(this.world, chpos, true)); + } + if (CHUNK_LOADED.isNeeded()) + { + boolean generated = chunkHolder.getCurrentChunk().getStatus() != ChunkStatus.FULL; + ChunkPos chpos = chunkHolder.getPos(); + this.world.getServer().execute(() -> CHUNK_LOADED.onChunkEvent(this.world, chpos, generated)); + } + } + */ + + @Unique + private void addTicket(ChunkPos pos, ChunkStatus status) + { // UNKNOWN + this.distanceManager.addTicket(TicketType.UNKNOWN, pos, 33 + ChunkLevel.byStatus(status), pos); + } + + @Unique + private void addTicket(ChunkPos pos) + { + this.addTicket(pos, ChunkStatus.EMPTY); + } + + + /* + @Unique + private void addRelightTicket(ChunkPos pos) + { + this.distanceManager.addRegionTicket(TicketType.LIGHT, pos, 1, pos); + } + + @Override + public void releaseRelightTicket(ChunkPos pos) + { + this.mainThreadExecutor.tell(Util.name( + () -> this.distanceManager.removeRegionTicket(TicketType.LIGHT, pos, 1, pos), + () -> "release relight ticket " + pos + )); + } + */ + @Unique + private void tickTicketManager() + { + this.distanceManager.runAllUpdates((ChunkMap) (Object) this); + } + + @Unique + private Set getExistingChunks(Set requestedChunks) + { + Map regionCache = new HashMap<>(); + Set ret = new HashSet<>(); + + for (ChunkPos pos : requestedChunks) + { + if (WorldTools.canHasChunk(this.level, pos, regionCache, true)) + { + ret.add(pos); + } + } + + return ret; + } + + + /* + @Unique + private Set loadExistingChunksFromDisk(Set requestedChunks) + { + Set existingChunks = this.getExistingChunks(requestedChunks); + for (ChunkPos pos : existingChunks) + { + this.updatingChunkMap.get(pos.toLong()).getOrScheduleFuture(ChunkStatus.EMPTY, (ChunkMap) (Object) this); + } + + return existingChunks; + } + + @Unique + private Set loadExistingChunks(Set requestedChunks, Object2IntMap report) + { + if (report != null) + { + report.put("requested_chunks", requestedChunks.size()); + } + + // Load all relevant ChunkHolders into this.currentChunkHolders + // This will not trigger loading from disk yet + + for (ChunkPos pos : requestedChunks) + { + this.addTicket(pos); + } + + this.tickTicketManager(); + + // Fetch all currently loaded chunks + + Set loadedChunks = requestedChunks.stream().filter( + pos -> this.updatingChunkMap.get(pos.toLong()).getLastAvailable() != null // all relevant ChunkHolders exist + ).collect(Collectors.toSet()); + + if (report != null) + { + report.put("loaded_chunks", loadedChunks.size()); + } + + // Load remaining chunks from disk + + Set unloadedChunks = new HashSet<>(requestedChunks); + unloadedChunks.removeAll(loadedChunks); + + Set existingChunks = this.loadExistingChunksFromDisk(unloadedChunks); + + existingChunks.addAll(loadedChunks); + + return existingChunks; + } + + @Unique + private Set loadExistingChunks(Set requestedChunks) + { + return this.loadExistingChunks(requestedChunks, null); + } + + @Unique + private void waitFor(Future future) + { + this.mainThreadExecutor.managedBlock(future::isDone); + } + + @Unique + private void waitFor(List> futures) + { + this.waitFor(Util.sequenceFailFast(futures)); + } + + @Unique + private ChunkAccess getCurrentChunk(ChunkPos pos) + { + CompletableFuture future = this.updatingChunkMap.get(pos.toLong()).getChunkToSave(); + this.waitFor(future); + + return future.join(); + } + + @Override + public void relightChunk(ChunkPos pos) + { + this.addTicket(pos); + this.tickTicketManager(); + if (this.updatingChunkMap.get(pos.toLong()).getLastAvailable() == null) // chunk unloaded + { + if (WorldTools.canHasChunk(this.level, pos, null, true)) + { + this.updatingChunkMap.get(pos.toLong()).getOrScheduleFuture(ChunkStatus.EMPTY, (ChunkMap) (Object) this); + } + } + ChunkAccess chunk = this.getCurrentChunk(pos); + if (!(chunk.getStatus().isOrAfter(ChunkStatus.LIGHT.getParent()))) + { + return; + } + ((ServerLightingProviderInterface) this.lightEngine).removeLightData(chunk); + this.addRelightTicket(pos); + ChunkHolder chunkHolder = this.updatingChunkMap.get(pos.toLong()); + CompletableFuture lightFuture = this.getChunkRangeFuture(chunkHolder, 1, (pos_) -> ChunkStatus.LIGHT) + .thenCompose(results -> { + List depList = results.orElse(null); + if (depList == null) + { + this.releaseRelightTicket(pos); + return CompletableFuture.completedFuture(ChunkResult.error(results::getError)); + } + ((ServerLightingProviderInterface) this.lightEngine).relight(chunk); + return CompletableFuture.completedFuture(ChunkResult.of(depList)); + } + ); + this.waitFor(lightFuture); + } + + /* + + @Override + public Map regenerateChunkRegion(List requestedChunksList) + { + Object2IntMap report = new Object2IntOpenHashMap<>(); + Set requestedChunks = new HashSet<>(requestedChunksList); + + // Load requested chunks + + Set existingChunks = this.loadExistingChunks(requestedChunks, report); + + // Finish pending generation stages + // This ensures that no generation events will be put back on the main thread after the chunks have been deleted + + Set affectedChunks = new HashSet<>(); + + for (ChunkPos pos : existingChunks) + { + affectedChunks.add(this.getCurrentChunk(pos)); + } + + report.put("affected_chunks", affectedChunks.size()); + + // Load neighbors for light removal + + Set neighbors = new HashSet<>(); + + for (ChunkAccess chunk : affectedChunks) + { + ChunkPos pos = chunk.getPos(); + + for (int x = -1; x <= 1; ++x) + { + for (int z = -1; z <= 1; ++z) + { + if (x != 0 || z != 0) + { + ChunkPos nPos = new ChunkPos(pos.x + x, pos.z + z); + if (!requestedChunks.contains(nPos)) + { + neighbors.add(nPos); + } + } + } + } + } + + this.loadExistingChunks(neighbors); + + // Determine affected neighbors + + Set affectedNeighbors = new HashSet<>(); + + for (ChunkPos pos : neighbors) + { + ChunkAccess chunk = this.getCurrentChunk(pos); + + if (chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT.getParent())) + { + affectedNeighbors.add(chunk); + } + } + + // Unload affected chunks + + for (ChunkAccess chunk : affectedChunks) + { + ChunkPos pos = chunk.getPos(); + + // remove entities + long longPos = pos.toLong(); + if (chunk instanceof LevelChunk) + { + ((SimpleEntityLookupInterface) ((ServerWorldInterface) level).getEntityLookupCMPublic()).getChunkEntities(pos).forEach(entity -> { + if (!(entity instanceof Player)) + { + entity.discard(); + } + }); + } + + + if (chunk instanceof LevelChunk) + { + ((LevelChunk) chunk).setLoaded(false); + } + + if (chunk instanceof LevelChunk) + { + this.level.unload((LevelChunk) chunk); // block entities only + } + + ((ServerLightingProviderInterface) this.lightEngine).invokeUpdateChunkStatus(pos); + ((ServerLightingProviderInterface) this.lightEngine).removeLightData(chunk); + + this.progressListener.onStatusChange(pos, null); + } + + // Replace ChunkHolders + + for (ChunkAccess chunk : affectedChunks) + { + ChunkPos cPos = chunk.getPos(); + long pos = cPos.toLong(); + + ChunkHolder oldHolder = this.updatingChunkMap.remove(pos); + ChunkHolder newHolder = new ChunkHolder(cPos, oldHolder.getTicketLevel(), level, this.lightEngine, this.queueSorter, (ChunkHolder.PlayerProvider) this); + ((ChunkHolderInterface) newHolder).setDefaultProtoChunk(cPos, this.mainThreadExecutor, level); // enable chunk blending? + this.updatingChunkMap.put(pos, newHolder); + + ((ChunkTicketManagerInterface) this.distanceManager).replaceHolder(oldHolder, newHolder); + } + + this.modified = true; + this.promoteChunkMap(); + + + // Force generation to previous states + // This ensures that the world is in a consistent state after this method + // Also, this is needed to ensure chunks are saved to disk + + Map targetGenerationStatus = affectedChunks.stream().collect( + Collectors.toMap(ChunkAccess::getPos, ChunkAccess::getPersistedStatus) + ); + + for (Entry entry : targetGenerationStatus.entrySet()) + { + this.addTicket(entry.getKey(), entry.getValue()); + } + + this.tickTicketManager(); + + List>> targetGenerationFutures = new ArrayList<>(); + + for (Entry entry : targetGenerationStatus.entrySet()) + { + targetGenerationFutures.add(Pair.of( + entry.getValue(), + this.updatingChunkMap.get(entry.getKey().toLong()).getOrScheduleFuture(entry.getValue(), (ChunkMap) (Object) this) + )); + } + + Map>> targetGenerationFuturesGrouped = targetGenerationFutures.stream().collect( + Collectors.groupingBy( + Pair::getKey, + Collectors.mapping( + Entry::getValue, + Collectors.toList() + ) + ) + ); + + for (ChunkStatus status : ChunkStatus.getStatusList()) + { + List> futures = targetGenerationFuturesGrouped.get(status); + + if (futures == null) + { + continue; + } + + String statusName = BuiltInRegistries.CHUNK_STATUS.getKey(status).getPath(); + + report.put("layer_count_" + statusName, futures.size()); + long start = System.currentTimeMillis(); + + this.waitFor(futures); + + report.put("layer_time_" + statusName, (int) (System.currentTimeMillis() - start)); + } + + + report.put("relight_count", affectedNeighbors.size()); + + // Remove light for affected neighbors + + for (ChunkAccess chunk : affectedNeighbors) + { + ((ServerLightingProviderInterface) this.lightEngine).removeLightData(chunk); + } + + // Schedule relighting of neighbors + + for (ChunkAccess chunk : affectedNeighbors) + { + this.addRelightTicket(chunk.getPos()); + } + + this.tickTicketManager(); + + List> lightFutures = new ArrayList<>(); + + for (ChunkAccess chunk : affectedNeighbors) + { + ChunkPos pos = chunk.getPos(); + + lightFutures.add(this.getChunkRangeFuture(this.updatingChunkMap.get(pos.toLong()), 1, (pos_) -> ChunkStatus.LIGHT).thenCompose( + results -> { + List depList = results.orElse(null); + if (depList == null) + { + this.releaseRelightTicket(pos); + return CompletableFuture.completedFuture(ChunkResult.error(results::getError)); + } + ((ServerLightingProviderInterface) this.lightEngine).relight(chunk); + return CompletableFuture.completedFuture(ChunkResult.of(depList)); + } + )); + } + + + long relightStart = System.currentTimeMillis(); + + this.waitFor(lightFutures); + + report.put("relight_time", (int) (System.currentTimeMillis() - relightStart)); + + return report; + } + + + + @Override + public Iterable getChunksCM() + { + return getChunks(); + } + + */ +} diff --git a/src/main/java/carpet/mixins/ClientCommonPacketListenerImpl_customPacketMixin.java b/src/main/java/carpet/mixins/ClientCommonPacketListenerImpl_customPacketMixin.java new file mode 100644 index 0000000..c41fd42 --- /dev/null +++ b/src/main/java/carpet/mixins/ClientCommonPacketListenerImpl_customPacketMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.network.CarpetClient; +import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +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(ClientCommonPacketListenerImpl.class) +public class ClientCommonPacketListenerImpl_customPacketMixin +{ + @Inject(method = "onDisconnect", at = @At("HEAD")) + private void onCMDisconnected(DisconnectionDetails reason, CallbackInfo ci) + { + CarpetClient.disconnect(); + } + + @Inject(method = "handleCustomPayload(Lnet/minecraft/network/protocol/common/ClientboundCustomPayloadPacket;)V", + at = @At("HEAD")) + private void onOnCustomPayload(ClientboundCustomPayloadPacket packet, CallbackInfo ci) + { + //System.out.println("CustomPayload of : " + packet.payload()); + } + +} diff --git a/src/main/java/carpet/mixins/ClientPacketListener_clientCommandMixin.java b/src/main/java/carpet/mixins/ClientPacketListener_clientCommandMixin.java new file mode 100644 index 0000000..6690ea4 --- /dev/null +++ b/src/main/java/carpet/mixins/ClientPacketListener_clientCommandMixin.java @@ -0,0 +1,41 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import carpet.network.CarpetClient; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.CommonListenerCookie; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.Connection; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPacketListener.class) +public abstract class ClientPacketListener_clientCommandMixin extends ClientCommonPacketListenerImpl +{ + + protected ClientPacketListener_clientCommandMixin(final Minecraft minecraft, final Connection connection, final CommonListenerCookie commonListenerCookie) + { + super(minecraft, connection, commonListenerCookie); + } + + @Inject(method = "sendCommand", at = @At("HEAD")) + private void inspectMessage(String string, CallbackInfo ci) + { + if (string.startsWith("call ")) + { + String command = string.substring(5); + CarpetClient.sendClientCommand(command); + } + if (CarpetServer.minecraft_server == null && !CarpetClient.isCarpet() && minecraft.player != null) + { + LocalPlayer playerSource = minecraft.player; + CarpetServer.forEachManager(sm -> sm.inspectClientsideCommand(playerSource.createCommandSourceStack(), "/" + string)); + } + } +} diff --git a/src/main/java/carpet/mixins/ClientPacketListener_customPacketsMixin.java b/src/main/java/carpet/mixins/ClientPacketListener_customPacketsMixin.java new file mode 100644 index 0000000..4895460 --- /dev/null +++ b/src/main/java/carpet/mixins/ClientPacketListener_customPacketsMixin.java @@ -0,0 +1,44 @@ +package carpet.mixins; + +import carpet.network.CarpetClient; +import carpet.network.ClientNetworkHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientCommonPacketListenerImpl; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.multiplayer.CommonListenerCookie; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.network.protocol.game.ClientboundLoginPacket; +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(ClientPacketListener.class) +public abstract class ClientPacketListener_customPacketsMixin extends ClientCommonPacketListenerImpl +{ + + protected ClientPacketListener_customPacketsMixin(final Minecraft minecraft, final Connection connection, final CommonListenerCookie commonListenerCookie) + { + super(minecraft, connection, commonListenerCookie); + } + + @Inject(method = "handleLogin", at = @At("RETURN")) + private void onGameJoined(ClientboundLoginPacket packet, CallbackInfo info) + { + CarpetClient.gameJoined( minecraft.player); + } + + @Inject(method = "handleUnknownCustomPayload", at = @At( + value = "HEAD" + ), cancellable = true) + private void onOnCustomPayload(CustomPacketPayload packet, CallbackInfo ci) + { + if (packet instanceof CarpetClient.CarpetPayload cpp) + { + ClientNetworkHandler.onServerData(cpp.data(), minecraft.player); + ci.cancel(); + } + } + +} diff --git a/src/main/java/carpet/mixins/CloneCommands_fillUpdatesMixin.java b/src/main/java/carpet/mixins/CloneCommands_fillUpdatesMixin.java new file mode 100644 index 0000000..7a17244 --- /dev/null +++ b/src/main/java/carpet/mixins/CloneCommands_fillUpdatesMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.commands.CloneCommands; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(CloneCommands.class) +public abstract class CloneCommands_fillUpdatesMixin +{ + @Redirect(method = "clone", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;blockUpdated(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;)V" + )) + private static void conditionalUpdating(ServerLevel serverWorld, BlockPos blockPos_1, Block block_1) + { + if (CarpetSettings.fillUpdates) serverWorld.blockUpdated(blockPos_1, block_1); + } +} diff --git a/src/main/java/carpet/mixins/CollectingNeighborUpdaterAccessor.java b/src/main/java/carpet/mixins/CollectingNeighborUpdaterAccessor.java new file mode 100644 index 0000000..8e26218 --- /dev/null +++ b/src/main/java/carpet/mixins/CollectingNeighborUpdaterAccessor.java @@ -0,0 +1,13 @@ +package carpet.mixins; + +import net.minecraft.world.level.redstone.CollectingNeighborUpdater; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(CollectingNeighborUpdater.class) +public interface CollectingNeighborUpdaterAccessor { + @Accessor("count") + void setCount(int count); + @Accessor("maxChainedNeighborUpdates") + int getMaxChainedNeighborUpdates(); +} diff --git a/src/main/java/carpet/mixins/CommandDispatcher_scarpetCommandsMixin.java b/src/main/java/carpet/mixins/CommandDispatcher_scarpetCommandsMixin.java new file mode 100644 index 0000000..d2e5cd1 --- /dev/null +++ b/src/main/java/carpet/mixins/CommandDispatcher_scarpetCommandsMixin.java @@ -0,0 +1,22 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.RootCommandNode; + +import carpet.fakes.CommandDispatcherInterface; +import carpet.fakes.CommandNodeInterface; + +@Mixin(value = CommandDispatcher.class, remap = false) +public class CommandDispatcher_scarpetCommandsMixin implements CommandDispatcherInterface { + @Shadow @Final + private RootCommandNode root; + + @Override + public void carpet$unregister(String node) { + ((CommandNodeInterface)this.root).carpet$removeChild(node); + } +} diff --git a/src/main/java/carpet/mixins/CommandNode_scarpetCommandsMixin.java b/src/main/java/carpet/mixins/CommandNode_scarpetCommandsMixin.java new file mode 100644 index 0000000..642f261 --- /dev/null +++ b/src/main/java/carpet/mixins/CommandNode_scarpetCommandsMixin.java @@ -0,0 +1,30 @@ +package carpet.mixins; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import carpet.fakes.CommandNodeInterface; + +@Mixin(value = CommandNode.class, remap = false) +public class CommandNode_scarpetCommandsMixin implements CommandNodeInterface { + @Shadow @Final + private Map> children; + @Shadow @Final + private Map> literals; + @Shadow @Final + private Map> arguments; + + @Override + public void carpet$removeChild(String name) { + this.children.remove(name); + this.literals.remove(name); + this.arguments.remove(name); + } +} diff --git a/src/main/java/carpet/mixins/Commands_customCommandsMixin.java b/src/main/java/carpet/mixins/Commands_customCommandsMixin.java new file mode 100644 index 0000000..88de814 --- /dev/null +++ b/src/main/java/carpet/mixins/Commands_customCommandsMixin.java @@ -0,0 +1,58 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Commands.class) +public abstract class Commands_customCommandsMixin +{ + + @Shadow + @Final + private CommandDispatcher dispatcher; + + @Inject(method = "", at = @At("RETURN")) + private void onRegister(Commands.CommandSelection commandSelection, CommandBuildContext commandBuildContext, CallbackInfo ci) { + CarpetServer.registerCarpetCommands(this.dispatcher, commandSelection, commandBuildContext); + } + + @Inject(method = "performCommand", at = @At("HEAD")) + private void onExecuteBegin(ParseResults parseResults, String string, CallbackInfo ci) + { + if (!CarpetSettings.fillUpdates) + CarpetSettings.impendingFillSkipUpdates.set(true); + } + + @Inject(method = "performCommand", at = @At("RETURN")) + private void onExecuteEnd(ParseResults parseResults, String string, CallbackInfo ci) + { + CarpetSettings.impendingFillSkipUpdates.set(false); + } + + @Redirect(method = "performCommand", at = @At( + value = "INVOKE", + target = "Lorg/slf4j/Logger;isDebugEnabled()Z", + remap = false + ), + require = 0 + ) + private boolean doesOutputCommandStackTrace(Logger logger) + { + if (CarpetSettings.superSecretSetting) + return true; + return logger.isDebugEnabled(); + } +} diff --git a/src/main/java/carpet/mixins/Connection_packetCounterMixin.java b/src/main/java/carpet/mixins/Connection_packetCounterMixin.java new file mode 100644 index 0000000..c7159e7 --- /dev/null +++ b/src/main/java/carpet/mixins/Connection_packetCounterMixin.java @@ -0,0 +1,39 @@ +package carpet.mixins; + +import carpet.fakes.ClientConnectionInterface; +import carpet.logging.logHelpers.PacketCounter; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.protocol.Packet; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Connection.class) +public abstract class Connection_packetCounterMixin implements ClientConnectionInterface +{ + // Add to the packet counter whenever a packet is received. + @Inject(method = "channelRead0", at = @At("HEAD")) + private void packetInCount(ChannelHandlerContext channelHandlerContext_1, Packet packet_1, CallbackInfo ci) + { + PacketCounter.totalIn++; + } + + // Add to the packet counter whenever a packet is sent. + @Inject(method = "sendPacket", at = @At("HEAD")) + private void packetOutCount(final Packet packet, final PacketSendListener packetSendListener, final boolean bl, final CallbackInfo ci) + { + PacketCounter.totalOut++; + } + + @Override + @Accessor //Compat with adventure-platform-fabric + public abstract void setChannel(Channel channel); +} diff --git a/src/main/java/carpet/mixins/CoralFanBlock_renewableCoralMixin.java b/src/main/java/carpet/mixins/CoralFanBlock_renewableCoralMixin.java new file mode 100644 index 0000000..d1d072c --- /dev/null +++ b/src/main/java/carpet/mixins/CoralFanBlock_renewableCoralMixin.java @@ -0,0 +1,16 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.helpers.FertilizableCoral; +import net.minecraft.world.level.block.CoralFanBlock; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(CoralFanBlock.class) +public class CoralFanBlock_renewableCoralMixin implements FertilizableCoral +{ + @Override + public boolean isEnabled() { + return CarpetSettings.renewableCoral == CarpetSettings.RenewableCoralMode.EXPANDED; + } + // Logic in FertilizableCoral +} diff --git a/src/main/java/carpet/mixins/CoralFeature_renewableCoralMixin.java b/src/main/java/carpet/mixins/CoralFeature_renewableCoralMixin.java new file mode 100644 index 0000000..f2d3800 --- /dev/null +++ b/src/main/java/carpet/mixins/CoralFeature_renewableCoralMixin.java @@ -0,0 +1,26 @@ +package carpet.mixins; + +import carpet.fakes.CoralFeatureInterface; +import net.minecraft.util.RandomSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Random; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.CoralFeature; + +@Mixin(CoralFeature.class) +public abstract class CoralFeature_renewableCoralMixin implements CoralFeatureInterface +{ + + @Shadow protected abstract boolean placeFeature(LevelAccessor var1, RandomSource var2, BlockPos var3, BlockState var4); + + @Override + public boolean growSpecific(Level worldIn, RandomSource random, BlockPos pos, BlockState blockUnder) + { + return placeFeature(worldIn, random, pos, blockUnder); + } +} diff --git a/src/main/java/carpet/mixins/CoralPlantBlock_renewableCoralMixin.java b/src/main/java/carpet/mixins/CoralPlantBlock_renewableCoralMixin.java new file mode 100644 index 0000000..4bf821a --- /dev/null +++ b/src/main/java/carpet/mixins/CoralPlantBlock_renewableCoralMixin.java @@ -0,0 +1,18 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.helpers.FertilizableCoral; +import org.spongepowered.asm.mixin.Mixin; + +import net.minecraft.world.level.block.CoralPlantBlock; + +@Mixin(CoralPlantBlock.class) +public class CoralPlantBlock_renewableCoralMixin implements FertilizableCoral +{ + @Override + public boolean isEnabled() { + return CarpetSettings.renewableCoral == CarpetSettings.RenewableCoralMode.EXPANDED + || CarpetSettings.renewableCoral == CarpetSettings.RenewableCoralMode.TRUE; + } + // Logic in FertilizableCoral +} diff --git a/src/main/java/carpet/mixins/CustomPacketPayload_networkStuffMixin.java b/src/main/java/carpet/mixins/CustomPacketPayload_networkStuffMixin.java new file mode 100644 index 0000000..7e9c88e --- /dev/null +++ b/src/main/java/carpet/mixins/CustomPacketPayload_networkStuffMixin.java @@ -0,0 +1,31 @@ +package carpet.mixins; + +import carpet.helpers.CarpetTaintedList; +import carpet.network.CarpetClient; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +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 java.util.List; + +@Mixin(CustomPacketPayload.class) +public interface CustomPacketPayload_networkStuffMixin +{ + @Inject(method = "codec(Lnet/minecraft/network/protocol/common/custom/CustomPacketPayload$FallbackProvider;Ljava/util/List;)Lnet/minecraft/network/codec/StreamCodec;", at = @At("HEAD"), cancellable = true) + private static void onCodec(final CustomPacketPayload.FallbackProvider fallbackProvider, final List> list, final CallbackInfoReturnable> cir) + { + // this is stupid hack to make sure carpet payloads are always registered + // that might collide with other mods that do the same thing + // so we may need to adjust this in the future + if (!(list instanceof CarpetTaintedList)) + { + List> extendedList = new CarpetTaintedList<>(list); + extendedList.add(new CustomPacketPayload.TypeAndCodec<>(CarpetClient.CarpetPayload.TYPE, CarpetClient.CarpetPayload.STREAM_CODEC)); + cir.setReturnValue(CustomPacketPayload.codec(fallbackProvider, extendedList)); + } + } +} 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 0000000..d7030e8 --- /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.renderer.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 = "clear", at = @At("HEAD")) + private void resetScarpetRenderes(CallbackInfo ci) + { + if (CarpetClient.shapes != null) + CarpetClient.shapes.reset(); + } +} diff --git a/src/main/java/carpet/mixins/DirectionMixin.java b/src/main/java/carpet/mixins/DirectionMixin.java new file mode 100644 index 0000000..ac5d14e --- /dev/null +++ b/src/main/java/carpet/mixins/DirectionMixin.java @@ -0,0 +1,43 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.EntityInterface; +import carpet.helpers.BlockRotator; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Direction.class) +public abstract class DirectionMixin +{ + @Redirect(method = "orderedByNearest", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getViewYRot(F)F")) + private static float getYaw(Entity entity, float float_1) + { + float yaw; + if (!CarpetSettings.placementRotationFix) + { + yaw = entity.getViewYRot(float_1); + } + else + { + yaw = ((EntityInterface) entity).getMainYaw(float_1); + } + if (BlockRotator.flippinEligibility(entity)) + { + yaw += 180f; + } + return yaw; + } + @Redirect(method = "orderedByNearest", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;getViewXRot(F)F")) + private static float getPitch(Entity entity, float float_1) + { + float pitch = entity.getViewXRot(float_1); + if (BlockRotator.flippinEligibility(entity)) + { + pitch = -pitch; + } + return pitch; + } +} diff --git a/src/main/java/carpet/mixins/DispenserBlock_cactusMixin.java b/src/main/java/carpet/mixins/DispenserBlock_cactusMixin.java new file mode 100644 index 0000000..12bd926 --- /dev/null +++ b/src/main/java/carpet/mixins/DispenserBlock_cactusMixin.java @@ -0,0 +1,25 @@ +package carpet.mixins; + +import carpet.helpers.BlockRotator; +import net.minecraft.core.dispenser.DispenseItemBehavior; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.DispenserBlock; +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(DispenserBlock.class) +public abstract class DispenserBlock_cactusMixin +{ + @Inject(method = "getDispenseMethod", at = @At("HEAD"), cancellable = true) + private void registerCarpetBehaviors(Level level, ItemStack stack, CallbackInfoReturnable cir) + { + Item item = stack.getItem(); + if (item == Items.CACTUS) + cir.setReturnValue(new BlockRotator.CactusDispenserBehaviour()); + } +} diff --git a/src/main/java/carpet/mixins/DispenserBlock_qcMixin.java b/src/main/java/carpet/mixins/DispenserBlock_qcMixin.java new file mode 100644 index 0000000..6444c1a --- /dev/null +++ b/src/main/java/carpet/mixins/DispenserBlock_qcMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import carpet.helpers.QuasiConnectivity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(DispenserBlock.class) +public class DispenserBlock_qcMixin { + + @Redirect( + method = "neighborChanged", + at = @At( + value = "INVOKE", + ordinal = 1, + target = "Lnet/minecraft/world/level/Level;hasNeighborSignal(Lnet/minecraft/core/BlockPos;)Z" + ) + ) + private boolean carpet_hasQuasiSignal(Level _level, BlockPos above, BlockState state, Level level, BlockPos pos, Block neighborBlock, BlockPos neighborPos, boolean movedByPiston) { + return QuasiConnectivity.hasQuasiSignal(level, pos); + } +} diff --git a/src/main/java/carpet/mixins/Display_scarpetEventMixin.java b/src/main/java/carpet/mixins/Display_scarpetEventMixin.java new file mode 100644 index 0000000..c69feb0 --- /dev/null +++ b/src/main/java/carpet/mixins/Display_scarpetEventMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +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(Display.class) +public abstract class Display_scarpetEventMixin extends Entity +{ + public Display_scarpetEventMixin(final EntityType entityType, final Level level) + { + super(entityType, level); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + // calling extra on_tick because displays don't tick + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_TICK); + } +} diff --git a/src/main/java/carpet/mixins/DistanceManager_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/DistanceManager_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..694aed4 --- /dev/null +++ b/src/main/java/carpet/mixins/DistanceManager_scarpetChunkCreationMixin.java @@ -0,0 +1,25 @@ +package carpet.mixins; + +import java.util.Set; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.DistanceManager; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import carpet.fakes.ChunkTicketManagerInterface; + +@Mixin(DistanceManager.class) +public abstract class DistanceManager_scarpetChunkCreationMixin implements ChunkTicketManagerInterface +{ + @Shadow + @Final + private Set chunksToUpdateFutures; + + @Override + public void replaceHolder(final ChunkHolder oldHolder, final ChunkHolder newHolder) + { + this.chunksToUpdateFutures.remove(oldHolder); + this.chunksToUpdateFutures.add(newHolder); + } +} diff --git a/src/main/java/carpet/mixins/DistanceManager_scarpetMixin.java b/src/main/java/carpet/mixins/DistanceManager_scarpetMixin.java new file mode 100644 index 0000000..120f1f4 --- /dev/null +++ b/src/main/java/carpet/mixins/DistanceManager_scarpetMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.fakes.ChunkTicketManagerInterface; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.Ticket; +import net.minecraft.util.SortedArraySet; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(DistanceManager.class) +public abstract class DistanceManager_scarpetMixin implements ChunkTicketManagerInterface +{ + @Shadow @Final private Long2ObjectOpenHashMap>> tickets; + + @Override + public Long2ObjectOpenHashMap>> getTicketsByPosition() + { + return tickets; + } + +} diff --git a/src/main/java/carpet/mixins/DistanceManager_spawnChunksMixin.java b/src/main/java/carpet/mixins/DistanceManager_spawnChunksMixin.java new file mode 100644 index 0000000..e4a24c6 --- /dev/null +++ b/src/main/java/carpet/mixins/DistanceManager_spawnChunksMixin.java @@ -0,0 +1,55 @@ +package carpet.mixins; + +import carpet.fakes.ChunkTicketManagerInterface; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.SortedArraySet; +import net.minecraft.util.Unit; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Iterator; + +@Mixin(DistanceManager.class) +public abstract class DistanceManager_spawnChunksMixin implements ChunkTicketManagerInterface +{ + @Shadow @Final private Long2ObjectOpenHashMap>> tickets; + + @Shadow protected abstract void removeTicket(long pos, Ticket ticket); + + @Shadow public abstract void addRegionTicket(TicketType type, ChunkPos pos, int radius, T argument); + + @Override + public void changeSpawnChunks(ChunkPos chunkPos, int distance) + { + long pos = chunkPos.toLong(); + SortedArraySet> set = tickets.get(pos); + Ticket existingTicket = null; + if (set != null) + { + Iterator> iter = set.iterator(); + while(iter.hasNext()) + { + Ticket ticket = iter.next(); + if (ticket.getType() == TicketType.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) + addRegionTicket(TicketType.START, chunkPos, distance, Unit.INSTANCE); + } +} diff --git a/src/main/java/carpet/mixins/DynamicGraphMinFixedPoint_resetChunkInterface.java b/src/main/java/carpet/mixins/DynamicGraphMinFixedPoint_resetChunkInterface.java new file mode 100644 index 0000000..cc8f8e0 --- /dev/null +++ b/src/main/java/carpet/mixins/DynamicGraphMinFixedPoint_resetChunkInterface.java @@ -0,0 +1,15 @@ +package carpet.mixins; + +import net.minecraft.world.level.lighting.DynamicGraphMinFixedPoint; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(DynamicGraphMinFixedPoint.class) +public interface DynamicGraphMinFixedPoint_resetChunkInterface +{ + @Invoker("checkEdge") + void cmInvokeUpdateLevel(long sourceId, long id, int level, boolean decrease); + + @Invoker("computeLevelFromNeighbor") + int cmCallGetPropagatedLevel(long sourceId, long targetId, int level); +} diff --git a/src/main/java/carpet/mixins/EndCrystal_scarpetEventsMixin.java b/src/main/java/carpet/mixins/EndCrystal_scarpetEventsMixin.java new file mode 100644 index 0000000..d7d261a --- /dev/null +++ b/src/main/java/carpet/mixins/EndCrystal_scarpetEventsMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.boss.enderdragon.EndCrystal; +import net.minecraft.world.level.Level; +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(EndCrystal.class) +public abstract class EndCrystal_scarpetEventsMixin extends Entity +{ + public EndCrystal_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + // calling extra on_tick because falling blocks do not fall back to super tick call + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_TICK); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/EntityMixin.java b/src/main/java/carpet/mixins/EntityMixin.java new file mode 100644 index 0000000..6c2223a --- /dev/null +++ b/src/main/java/carpet/mixins/EntityMixin.java @@ -0,0 +1,40 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.patches.EntityPlayerMPFake; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Entity.class) +public abstract class EntityMixin implements EntityInterface +{ + @Shadow + public float yRot; + + @Shadow + public float yRotO; + + @Shadow public @Nullable abstract LivingEntity getControllingPassenger(); + + @Shadow public Level level; + + @Override + public float getMainYaw(float partialTicks) + { + return partialTicks == 1.0F ? this.yRot : Mth.lerp(partialTicks, this.yRotO, this.yRot); + } + + @Inject(method = "isControlledByLocalInstance", at = @At("HEAD"), cancellable = true) + private void isFakePlayer(CallbackInfoReturnable cir) + { + if (getControllingPassenger() instanceof EntityPlayerMPFake) cir.setReturnValue(!level.isClientSide); + } +} diff --git a/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java b/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java new file mode 100644 index 0000000..ea70b6b --- /dev/null +++ b/src/main/java/carpet/mixins/Entity_scarpetEventsMixin.java @@ -0,0 +1,103 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.fakes.PortalProcessorInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.PortalProcessor; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +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(Entity.class) +public abstract class Entity_scarpetEventsMixin implements EntityInterface +{ + //@Shadow public boolean removed; + + @Shadow private int portalCooldown; + + @Shadow public abstract boolean isRemoved(); + + @Shadow private Vec3 position, deltaMovement; + + @Shadow @Nullable public PortalProcessor portalProcess; + private boolean permanentVehicle; + + private final EntityEventsGroup events = new EntityEventsGroup((Entity) (Object)this); + + private Vec3 pos1, motion; + + @Override + public EntityEventsGroup getEventContainer() + { + return events; + } + + @Override + public boolean isPermanentVehicle() + { + return permanentVehicle; + } + + @Override + public void setPermanentVehicle(boolean permanent) + { + permanentVehicle = permanent; + } + + @Override + public int getPublicNetherPortalCooldown() + { + return portalCooldown; + } + + @Override + public void setPublicNetherPortalCooldown(int what) + { + portalCooldown = what; + } + + @Override + public int getPortalTimer() + { + return portalProcess.getPortalTime(); + } + + @Override + public void setPortalTimer(int amount) + { + ((PortalProcessorInterface)portalProcess).setPortalTime(amount); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + events.onEvent(EntityEventsGroup.Event.ON_TICK); + } + + + @Inject(method = "remove", at = @At("HEAD")) + private void onRemove(CallbackInfo ci) + { + if (!isRemoved()) events.onEvent(EntityEventsGroup.Event.ON_REMOVED); // ! isRemoved() + } + + + @Inject(method = "setPosRaw", at = @At("HEAD")) + private void firstPos(CallbackInfo ci) + { + pos1 = this.position; + motion = this.deltaMovement; + } + + @Inject(method = "setPosRaw", at = @At("TAIL")) + private void secondPos(CallbackInfo ci) + { + if(pos1!=this.position) + events.onEvent(EntityEventsGroup.Event.ON_MOVE, motion, pos1, this.position); + } +} diff --git a/src/main/java/carpet/mixins/ExperienceOrb_xpNoCooldownMixin.java b/src/main/java/carpet/mixins/ExperienceOrb_xpNoCooldownMixin.java new file mode 100644 index 0000000..0b04c22 --- /dev/null +++ b/src/main/java/carpet/mixins/ExperienceOrb_xpNoCooldownMixin.java @@ -0,0 +1,47 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ExperienceOrb; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +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(ExperienceOrb.class) +public abstract class ExperienceOrb_xpNoCooldownMixin extends Entity +{ + @Shadow + private int count; + + @Shadow + private int value; + + public ExperienceOrb_xpNoCooldownMixin(EntityType type, Level world) + { + super(type, world); + } + + @Shadow + protected abstract int repairPlayerItems(ServerPlayer player, int amount); + + @Inject(method = "playerTouch", at = @At("HEAD")) + private void addXP(Player player, CallbackInfo ci) { + if (CarpetSettings.xpNoCooldown && !level().isClientSide) { + player.takeXpDelay = 0; + // reducing the count to 1 and leaving vanilla to deal with it + while (this.count > 1) { + int remainder = this.repairPlayerItems((ServerPlayer) player, this.value); + if (remainder > 0) { + player.giveExperiencePoints(remainder); + } + this.count--; + } + } + } +} diff --git a/src/main/java/carpet/mixins/ExplosionAccessor.java b/src/main/java/carpet/mixins/ExplosionAccessor.java new file mode 100644 index 0000000..5b0875b --- /dev/null +++ b/src/main/java/carpet/mixins/ExplosionAccessor.java @@ -0,0 +1,45 @@ +package carpet.mixins; + +import net.minecraft.util.RandomSource; +import net.minecraft.world.damagesource.DamageSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; + +@Mixin(Explosion.class) +public interface ExplosionAccessor { + + @Accessor + boolean isFire(); + + @Accessor + Explosion.BlockInteraction getBlockInteraction(); + + @Accessor + Level getLevel(); + + @Accessor + RandomSource getRandom(); + + @Accessor + double getX(); + + @Accessor + double getY(); + + @Accessor + double getZ(); + + @Accessor + float getRadius(); + + @Accessor + Entity getSource(); + + @Accessor + DamageSource getDamageSource(); + +} diff --git a/src/main/java/carpet/mixins/Explosion_optimizedTntMixin.java b/src/main/java/carpet/mixins/Explosion_optimizedTntMixin.java new file mode 100644 index 0000000..3cf32d6 --- /dev/null +++ b/src/main/java/carpet/mixins/Explosion_optimizedTntMixin.java @@ -0,0 +1,103 @@ +package carpet.mixins; + +import carpet.helpers.OptimizedExplosion; +import carpet.CarpetSettings; +import carpet.logging.LoggerRegistry; +import carpet.logging.logHelpers.ExplosionLogHelper; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.breeze.Breeze; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +@Mixin(value = Explosion.class) +public abstract class Explosion_optimizedTntMixin +{ + @Shadow + @Final + private ObjectArrayList toBlow; + + @Shadow @Final private Level level; + + @Shadow @Nullable public abstract LivingEntity getIndirectSourceEntity(); + + private ExplosionLogHelper eLogger; + + @Inject(method = "explode", at = @At("HEAD"), + cancellable = true) + private void onExplosionA(CallbackInfo ci) + { + if (CarpetSettings.optimizedTNT && !level.isClientSide && !(getIndirectSourceEntity() instanceof Breeze)) + { + OptimizedExplosion.doExplosionA((Explosion) (Object) this, eLogger); + ci.cancel(); + } + } + + @Inject(method = "finalizeExplosion", at = @At("HEAD"), + cancellable = true) + private void onExplosionB(boolean spawnParticles, CallbackInfo ci) + { + if (eLogger != null) + { + eLogger.setAffectBlocks( ! toBlow.isEmpty()); + eLogger.onExplosionDone(this.level.getGameTime()); + } + if (CarpetSettings.explosionNoBlockDamage) + { + toBlow.clear(); + } + if (CarpetSettings.optimizedTNT && !level.isClientSide && !(getIndirectSourceEntity() instanceof Breeze)) + { + OptimizedExplosion.doExplosionB((Explosion) (Object) this, spawnParticles); + ci.cancel(); + } + } + //optional due to Overwrite in Lithium + //should kill most checks if no block damage is requested + @Redirect(method = "explode", require = 0, at = @At(value = "INVOKE", + target ="Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;")) + private BlockState noBlockCalcsWithNoBLockDamage(Level world, BlockPos pos) + { + if (CarpetSettings.explosionNoBlockDamage) return Blocks.BEDROCK.defaultBlockState(); + return world.getBlockState(pos); + } + + @Inject(method = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lnet/minecraft/world/level/ExplosionDamageCalculator;DDDFZLnet/minecraft/world/level/Explosion$BlockInteraction;Lnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/Holder;)V", + at = @At(value = "RETURN")) + private void onExplosionCreated(Level world, Entity entity, DamageSource damageSource, ExplosionDamageCalculator explosionBehavior, double x, double y, double z, float power, boolean createFire, Explosion.BlockInteraction destructionType, ParticleOptions particleOptions, ParticleOptions particleOptions2, Holder soundEvent, CallbackInfo ci) + { + if (LoggerRegistry.__explosions && ! world.isClientSide) + { + eLogger = new ExplosionLogHelper(x, y, z, power, createFire, destructionType, level.registryAccess()); + } + } + + @Redirect(method = "explode", + at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;setDeltaMovement(Lnet/minecraft/world/phys/Vec3;)V")) + private void setVelocityAndUpdateLogging(Entity entity, Vec3 velocity) + { + if (eLogger != null) { + eLogger.onEntityImpacted(entity, velocity.subtract(entity.getDeltaMovement())); + } + entity.setDeltaMovement(velocity); + } +} diff --git a/src/main/java/carpet/mixins/Explosion_scarpetEventMixin.java b/src/main/java/carpet/mixins/Explosion_scarpetEventMixin.java new file mode 100644 index 0000000..12e9bf2 --- /dev/null +++ b/src/main/java/carpet/mixins/Explosion_scarpetEventMixin.java @@ -0,0 +1,79 @@ +package carpet.mixins; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleOptions; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import static carpet.script.CarpetEventServer.Event.EXPLOSION_OUTCOME; + +@Mixin(value = Explosion.class, priority = 990) +public abstract class Explosion_scarpetEventMixin +{ + @Shadow @Final private Level level; + @Shadow @Final private double x; + @Shadow @Final private double y; + @Shadow @Final private double z; + @Shadow @Final private float radius; + @Shadow @Final private boolean fire; + @Shadow @Final private ObjectArrayList toBlow; + @Shadow @Final private Explosion.BlockInteraction blockInteraction; + @Shadow @Final private @Nullable Entity source; + + @Shadow /*@Nullable*/ public abstract /*@Nullable*/ LivingEntity getIndirectSourceEntity(); + + @Shadow public static float getSeenPercent(Vec3 source, Entity entity) {return 0.0f;} + + private List affectedEntities; + + @Inject(method = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lnet/minecraft/world/level/ExplosionDamageCalculator;DDDFZLnet/minecraft/world/level/Explosion$BlockInteraction;Lnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/Holder;)V", + at = @At(value = "RETURN")) + private void onExplosionCreated(Level world, Entity entity, DamageSource damageSource, ExplosionDamageCalculator explosionBehavior, double x, double y, double z, float power, boolean createFire, Explosion.BlockInteraction destructionType, ParticleOptions particleOptions, ParticleOptions particleOptions2, Holder soundEvent, CallbackInfo ci) + { + if (EXPLOSION_OUTCOME.isNeeded() && !world.isClientSide()) + { + affectedEntities = new ArrayList<>(); + } + } + + @Redirect(method = "explode", at=@At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/Explosion;getSeenPercent(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/world/entity/Entity;)F") + ) + private float onExplosion(Vec3 source, Entity entity) + { + if (affectedEntities != null) + { + affectedEntities.add(entity); + } + return getSeenPercent(source, entity); + } + + @Inject(method = "finalizeExplosion", at = @At("HEAD")) + private void onExplosion(boolean spawnParticles, CallbackInfo ci) + { + if (EXPLOSION_OUTCOME.isNeeded() && !level.isClientSide()) + { + EXPLOSION_OUTCOME.onExplosion((ServerLevel) level, source, this::getIndirectSourceEntity, x, y, z, radius, fire, toBlow, affectedEntities, blockInteraction); + } + } +} diff --git a/src/main/java/carpet/mixins/Explosion_xpFromBlocksMixin.java b/src/main/java/carpet/mixins/Explosion_xpFromBlocksMixin.java new file mode 100644 index 0000000..866e982 --- /dev/null +++ b/src/main/java/carpet/mixins/Explosion_xpFromBlocksMixin.java @@ -0,0 +1,24 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(BlockBehaviour.class) +public class Explosion_xpFromBlocksMixin { + + @Redirect(method = "onExplosionHit", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;spawnAfterBreak(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/item/ItemStack;Z)V" + )) + private void spawnXPAfterBreak(BlockState instance, ServerLevel serverLevel, BlockPos blockPos, ItemStack itemStack, boolean b) + { + instance.spawnAfterBreak(serverLevel, blockPos, itemStack, b || CarpetSettings.xpFromExplosions); + } +} diff --git a/src/main/java/carpet/mixins/FallingBlockEntityMixin.java b/src/main/java/carpet/mixins/FallingBlockEntityMixin.java new file mode 100644 index 0000000..2e86c9b --- /dev/null +++ b/src/main/java/carpet/mixins/FallingBlockEntityMixin.java @@ -0,0 +1,42 @@ +package carpet.mixins; + +import carpet.logging.LoggerRegistry; +import carpet.logging.logHelpers.TrajectoryLogHelper; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.level.Level; +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(FallingBlockEntity.class) +public abstract class FallingBlockEntityMixin extends Entity +{ + private TrajectoryLogHelper logHelper; + public FallingBlockEntityMixin(EntityType entityType_1, Level world_1) { super(entityType_1, world_1); } + + @Inject(method = "(Lnet/minecraft/world/entity/EntityType;Lnet/minecraft/world/level/Level;)V", at = @At("RETURN")) + private void addLogger(EntityType entityType_1, Level world_1, CallbackInfo ci) + { + if (LoggerRegistry.__fallingBlocks && !world_1.isClientSide) + logHelper = new TrajectoryLogHelper("fallingBlocks"); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void tickCheck(CallbackInfo ci) + { + if (LoggerRegistry.__fallingBlocks && logHelper != null) + logHelper.onTick(getX(), getY(), getZ(), getDeltaMovement()); + } + + @Override + public void remove(Entity.RemovalReason arg) // reason + { + super.remove(arg); + if (LoggerRegistry.__fallingBlocks && logHelper != null) + logHelper.onFinish(); + } +} diff --git a/src/main/java/carpet/mixins/FallingBlockEntity_scarpetEventsMixin.java b/src/main/java/carpet/mixins/FallingBlockEntity_scarpetEventsMixin.java new file mode 100644 index 0000000..d3ee411 --- /dev/null +++ b/src/main/java/carpet/mixins/FallingBlockEntity_scarpetEventsMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.level.Level; +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(FallingBlockEntity.class) +public abstract class FallingBlockEntity_scarpetEventsMixin extends Entity +{ + public FallingBlockEntity_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + // calling extra on_tick because falling blocks do not fall back to super tick call + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_TICK); + } +} diff --git a/src/main/java/carpet/mixins/FillCommandMixin.java b/src/main/java/carpet/mixins/FillCommandMixin.java new file mode 100644 index 0000000..344f5ff --- /dev/null +++ b/src/main/java/carpet/mixins/FillCommandMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.commands.FillCommand; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(FillCommand.class) +public abstract class FillCommandMixin +{ + @Redirect(method = "fillBlocks", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;blockUpdated(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;)V" + )) + private static void conditionalUpdating(ServerLevel serverWorld, BlockPos blockPos_1, Block block_1) + { + if (CarpetSettings.fillUpdates) serverWorld.blockUpdated(blockPos_1, block_1); + } +} diff --git a/src/main/java/carpet/mixins/FlowingFluid_liquidDamageDisabledMixin.java b/src/main/java/carpet/mixins/FlowingFluid_liquidDamageDisabledMixin.java new file mode 100644 index 0000000..933e098 --- /dev/null +++ b/src/main/java/carpet/mixins/FlowingFluid_liquidDamageDisabledMixin.java @@ -0,0 +1,33 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.world.level.material.Fluid; +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(FlowingFluid.class) +public class FlowingFluid_liquidDamageDisabledMixin +{ + @Inject( + method = "canHoldFluid", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;blocksMotion()Z" + ), + cancellable = true + ) + private void stopBreakingBlock(BlockGetter world, BlockPos pos, BlockState state, Fluid fluid, CallbackInfoReturnable cir) + { + if (CarpetSettings.liquidDamageDisabled) + { + cir.setReturnValue(state.isAir() || state.is(Blocks.WATER) || state.is(Blocks.LAVA)); + } + } +} diff --git a/src/main/java/carpet/mixins/ForceLoadCommand_forceLoadLimitMixin.java b/src/main/java/carpet/mixins/ForceLoadCommand_forceLoadLimitMixin.java new file mode 100644 index 0000000..9ea69db --- /dev/null +++ b/src/main/java/carpet/mixins/ForceLoadCommand_forceLoadLimitMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.server.commands.ForceLoadCommand; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(ForceLoadCommand.class) +public class ForceLoadCommand_forceLoadLimitMixin +{ + @ModifyConstant(method = "changeForceLoad", constant = @Constant(longValue = 256L)) + private static long forceloadLimit(long original) + { + return CarpetSettings.forceloadLimit; + } + + @ModifyConstant(method = "changeForceLoad", constant = @Constant(intValue = 256)) + private static int forceloadLimitError(int original) + { + return CarpetSettings.forceloadLimit; + } +} diff --git a/src/main/java/carpet/mixins/Guardian_renewableSpongesMixin.java b/src/main/java/carpet/mixins/Guardian_renewableSpongesMixin.java new file mode 100644 index 0000000..42f5fda --- /dev/null +++ b/src/main/java/carpet/mixins/Guardian_renewableSpongesMixin.java @@ -0,0 +1,48 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.SpawnGroupData; +import net.minecraft.world.entity.monster.ElderGuardian; +import net.minecraft.world.entity.monster.Guardian; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(Guardian.class) +public abstract class Guardian_renewableSpongesMixin extends Monster +{ + protected Guardian_renewableSpongesMixin(EntityType entityType_1, Level world_1) + { + super(entityType_1, world_1); + } + + @Override + public void thunderHit(ServerLevel serverWorld, LightningBolt lightningEntity) + { // isRemoved() + if (!this.level().isClientSide && !this.isRemoved() && CarpetSettings.renewableSponges && !((Object)this instanceof ElderGuardian)) + { + ElderGuardian elderGuardian = new ElderGuardian(EntityType.ELDER_GUARDIAN ,this.level()); + elderGuardian.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); + elderGuardian.finalizeSpawn(serverWorld ,this.level().getCurrentDifficultyAt(elderGuardian.blockPosition()), MobSpawnType.CONVERSION, null); + elderGuardian.setNoAi(this.isNoAi()); + + if (this.hasCustomName()) + { + elderGuardian.setCustomName(this.getCustomName()); + elderGuardian.setCustomNameVisible(this.isCustomNameVisible()); + } + + this.level().addFreshEntity(elderGuardian); + this.discard(); // discard remove(); + } + else + { + super.thunderHit(serverWorld, lightningEntity); + } + } +} diff --git a/src/main/java/carpet/mixins/Gui_tablistMixin.java b/src/main/java/carpet/mixins/Gui_tablistMixin.java new file mode 100644 index 0000000..4723554 --- /dev/null +++ b/src/main/java/carpet/mixins/Gui_tablistMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.PlayerListHudInterface; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.components.PlayerTabOverlay; +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(Gui.class) +public abstract class Gui_tablistMixin +{ + @Shadow + @Final + private Minecraft minecraft; + + @Shadow @Final private PlayerTabOverlay tabList; + + @Redirect(method = "renderTabList", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isLocalServer()Z")) + private boolean onDraw(Minecraft minecraftClient) + { + return this.minecraft.isLocalServer() && !((PlayerListHudInterface) tabList).hasFooterOrHeader(); + } + +} diff --git a/src/main/java/carpet/mixins/HangingEntity_scarpetEventsMixin.java b/src/main/java/carpet/mixins/HangingEntity_scarpetEventsMixin.java new file mode 100644 index 0000000..0af3040 --- /dev/null +++ b/src/main/java/carpet/mixins/HangingEntity_scarpetEventsMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.decoration.BlockAttachedEntity; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.level.Level; +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(BlockAttachedEntity.class) +public abstract class HangingEntity_scarpetEventsMixin extends Entity +{ + public HangingEntity_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + // calling extra on_tick because falling blocks do not fall back to super tick call + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_TICK); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/HopperBlockEntity_counterMixin.java b/src/main/java/carpet/mixins/HopperBlockEntity_counterMixin.java new file mode 100644 index 0000000..cbec441 --- /dev/null +++ b/src/main/java/carpet/mixins/HopperBlockEntity_counterMixin.java @@ -0,0 +1,66 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.Direction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import carpet.helpers.HopperCounter; +import carpet.utils.WoolTool; +import net.minecraft.core.BlockPos; +import net.minecraft.world.Container; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.HopperBlock; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** + * The {@link Mixin} which removes items in a hopper if it points into a wool counter, and calls {@link HopperCounter#add} + */ +@Mixin(HopperBlockEntity.class) +public abstract class HopperBlockEntity_counterMixin extends RandomizableContainerBlockEntity +{ + protected HopperBlockEntity_counterMixin(BlockEntityType blockEntityType, BlockPos blockPos, BlockState blockState) { + super(blockEntityType, blockPos, blockState); + } + + //@Shadow public abstract int getContainerSize(); + + //@Shadow public abstract void setItem(int slot, ItemStack stack); + + @Shadow private Direction facing; + + /** + * A method to remove items from hoppers pointing into wool and count them via {@link HopperCounter#add} method + */ + @Inject(method = "ejectItems", at = @At("HEAD"), cancellable = true) + private static void onInsert(Level world, BlockPos blockPos, HopperBlockEntity hopperBlockEntity, CallbackInfoReturnable cir) + { + if (CarpetSettings.hopperCounters) { + Direction hopperFacing = world.getBlockState(blockPos).getValue(HopperBlock.FACING); + DyeColor woolColor = WoolTool.getWoolColorAtPosition( + world, + blockPos.relative(hopperFacing)); + if (woolColor != null) + { + Container inventory = HopperBlockEntity.getContainerAt(world, blockPos); + for (int i = 0; i < inventory.getContainerSize(); ++i) + { + if (!inventory.getItem(i).isEmpty()) + { + ItemStack itemstack = inventory.getItem(i);//.copy(); + HopperCounter.getCounter(woolColor).add(world.getServer(), itemstack); + inventory.setItem(i, ItemStack.EMPTY); + } + } + cir.setReturnValue(true); + } + } + } +} diff --git a/src/main/java/carpet/mixins/HopperBlock_cactusMixin.java b/src/main/java/carpet/mixins/HopperBlock_cactusMixin.java new file mode 100644 index 0000000..4394677 --- /dev/null +++ b/src/main/java/carpet/mixins/HopperBlock_cactusMixin.java @@ -0,0 +1,26 @@ +package carpet.mixins; + +import carpet.helpers.BlockRotator; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.HopperBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(HopperBlock.class) +public class HopperBlock_cactusMixin +{ + @Redirect(method = "getStateForPlacement", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/item/context/BlockPlaceContext;getClickedFace()Lnet/minecraft/core/Direction;" + )) + private Direction getOppositeOpposite(BlockPlaceContext context) + { + if (BlockRotator.flippinEligibility(context.getPlayer())) + { + return context.getClickedFace().getOpposite(); + } + return context.getClickedFace(); + } +} diff --git a/src/main/java/carpet/mixins/HorseBaseEntity_scarpetMixin.java b/src/main/java/carpet/mixins/HorseBaseEntity_scarpetMixin.java new file mode 100644 index 0000000..4e74292 --- /dev/null +++ b/src/main/java/carpet/mixins/HorseBaseEntity_scarpetMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.fakes.InventoryBearerInterface; +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(AbstractHorse.class) +public class HorseBaseEntity_scarpetMixin implements InventoryBearerInterface +{ + + @Shadow protected SimpleContainer inventory; + + @Override + public Container getCMInventory() + { + return inventory; + } +} diff --git a/src/main/java/carpet/mixins/HugeFungusFeatureMixin.java b/src/main/java/carpet/mixins/HugeFungusFeatureMixin.java new file mode 100644 index 0000000..e07921b --- /dev/null +++ b/src/main/java/carpet/mixins/HugeFungusFeatureMixin.java @@ -0,0 +1,24 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.feature.HugeFungusConfiguration; +import net.minecraft.world.level.levelgen.feature.HugeFungusFeature; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +import static carpet.CarpetSettings.FungusGrowthMode.*; + +@Mixin(HugeFungusFeature.class) +public class HugeFungusFeatureMixin { + @ModifyArgs(method = "place", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/feature/HugeFungusFeature;placeStem(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/util/RandomSource;Lnet/minecraft/world/level/levelgen/feature/HugeFungusConfiguration;Lnet/minecraft/core/BlockPos;IZ)V")) + private void mixin(Args args) { + boolean natural = !((HugeFungusConfiguration) args.get(2)).planted; + args.set(5, natural && ((boolean) args.get(5)) || + !natural && (CarpetSettings.thickFungusGrowth == ALL || + CarpetSettings.thickFungusGrowth == RANDOM && ((RandomSource) args.get(1)).nextFloat() < 0.06F) + ); + } +} diff --git a/src/main/java/carpet/mixins/Husk_templesMixin.java b/src/main/java/carpet/mixins/Husk_templesMixin.java new file mode 100644 index 0000000..9877200 --- /dev/null +++ b/src/main/java/carpet/mixins/Husk_templesMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.utils.SpawnOverrides; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.monster.Husk; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.levelgen.structure.BuiltinStructures; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Husk.class) +public class Husk_templesMixin +{ + @Redirect(method = "checkHuskSpawnRules", at = @At(value = "INVOKE", target="Lnet/minecraft/world/level/ServerLevelAccessor;canSeeSky(Lnet/minecraft/core/BlockPos;)Z")) + private static boolean isSkylightOrTempleVisible(ServerLevelAccessor serverWorldAccess, BlockPos pos) + { + return serverWorldAccess.canSeeSky(pos) || + (CarpetSettings.huskSpawningInTemples && SpawnOverrides.isStructureAtPosition((ServerLevel)serverWorldAccess, BuiltinStructures.DESERT_PYRAMID, pos)); + } +} diff --git a/src/main/java/carpet/mixins/InfestedBlock_gravelMixin.java b/src/main/java/carpet/mixins/InfestedBlock_gravelMixin.java new file mode 100644 index 0000000..c966346 --- /dev/null +++ b/src/main/java/carpet/mixins/InfestedBlock_gravelMixin.java @@ -0,0 +1,32 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.InfestedBlock; +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(InfestedBlock.class) +public abstract class InfestedBlock_gravelMixin extends Block +{ + public InfestedBlock_gravelMixin(Properties block$Settings_1) + { + super(block$Settings_1); + } + + @Inject(method = "spawnInfestation", at = @At(value = "INVOKE", shift = At.Shift.AFTER, + target = "Lnet/minecraft/world/entity/monster/Silverfish;spawnAnim()V")) + private void onOnStacksDropped(ServerLevel serverWorld, BlockPos pos, CallbackInfo ci) + { + if (CarpetSettings.silverFishDropGravel) + { + popResource(serverWorld, pos, new ItemStack(Blocks.GRAVEL)); + } + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/Ingredient_scarpetMixin.java b/src/main/java/carpet/mixins/Ingredient_scarpetMixin.java new file mode 100644 index 0000000..4d33697 --- /dev/null +++ b/src/main/java/carpet/mixins/Ingredient_scarpetMixin.java @@ -0,0 +1,24 @@ +package carpet.mixins; + +import carpet.fakes.IngredientInterface; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; + +@Mixin(Ingredient.class) +public class Ingredient_scarpetMixin implements IngredientInterface +{ + @Shadow @Final private Ingredient.Value[] values; + + @Override + public List> getRecipeStacks() + { + return Arrays.stream(values).map(Ingredient.Value::getItems).toList(); + } +} diff --git a/src/main/java/carpet/mixins/Inventory_scarpetEventMixin.java b/src/main/java/carpet/mixins/Inventory_scarpetEventMixin.java new file mode 100644 index 0000000..0c38c70 --- /dev/null +++ b/src/main/java/carpet/mixins/Inventory_scarpetEventMixin.java @@ -0,0 +1,40 @@ +package carpet.mixins; + +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; + +import static carpet.script.CarpetEventServer.Event.PLAYER_PICKS_UP_ITEM; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +@Mixin(Inventory.class) +public abstract class Inventory_scarpetEventMixin +{ + @Shadow @Final public Player player; + + @Redirect(method = "add(Lnet/minecraft/world/item/ItemStack;)Z", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Inventory;add(ILnet/minecraft/world/item/ItemStack;)Z" + )) + private boolean onItemAcquired(Inventory playerInventory, int slot, ItemStack stack) + { + if (!PLAYER_PICKS_UP_ITEM.isNeeded() || !(player instanceof ServerPlayer)) + return playerInventory.add(-1, stack); + int count = stack.getCount(); + ItemStack previous = stack.copy(); + boolean res = playerInventory.add(-1, stack); + if (count != stack.getCount()) // res returns false for larger item adding to a almost full ineventory + { + ItemStack diffStack = previous.copyWithCount(count - stack.getCount()); + PLAYER_PICKS_UP_ITEM.onItemAction((ServerPlayer) player, null, diffStack); + } + return res; + } + +} diff --git a/src/main/java/carpet/mixins/ItemEntityMixin.java b/src/main/java/carpet/mixins/ItemEntityMixin.java new file mode 100644 index 0000000..dbb247d --- /dev/null +++ b/src/main/java/carpet/mixins/ItemEntityMixin.java @@ -0,0 +1,39 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.Level; +import carpet.fakes.ItemEntityInterface; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ItemEntity.class) +public abstract class ItemEntityMixin extends Entity implements ItemEntityInterface +{ + @Shadow private int age; + @Shadow private int pickupDelay; + + public ItemEntityMixin(EntityType entityType_1, Level world_1) { + super(entityType_1, world_1); + } + + @Override + public void thunderHit(ServerLevel world, LightningBolt lightning) { + if (CarpetSettings.lightningKillsDropsFix) { + if (this.age > 8) { //Only kill item if it's older than 8 ticks + super.thunderHit(world, lightning); + } + } else { + super.thunderHit(world, lightning); + } + } + + @Override + public int getPickupDelayCM() { + return this.pickupDelay; + } +} diff --git a/src/main/java/carpet/mixins/ItemStack_stackableShulkerBoxesMixin.java b/src/main/java/carpet/mixins/ItemStack_stackableShulkerBoxesMixin.java new file mode 100644 index 0000000..4f11e6d --- /dev/null +++ b/src/main/java/carpet/mixins/ItemStack_stackableShulkerBoxesMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.block.ShulkerBoxBlock; +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(ItemStack.class) +public class ItemStack_stackableShulkerBoxesMixin +{ + @Inject(method = "getMaxStackSize", at = @At("HEAD"), cancellable = true) + private void getCMMAxStackSize(CallbackInfoReturnable cir) + { + if (CarpetSettings.shulkerBoxStackSize > 1 + && ((ItemStack)((Object)this)).getItem() instanceof BlockItem blockItem + && blockItem.getBlock() instanceof ShulkerBoxBlock + && ((ItemStack) ((Object) this)).getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).stream().findAny().isEmpty() + ) { + cir.setReturnValue(CarpetSettings.shulkerBoxStackSize); + } + } +} diff --git a/src/main/java/carpet/mixins/LavaFluid_renewableDeepslateMixin.java b/src/main/java/carpet/mixins/LavaFluid_renewableDeepslateMixin.java new file mode 100644 index 0000000..83229af --- /dev/null +++ b/src/main/java/carpet/mixins/LavaFluid_renewableDeepslateMixin.java @@ -0,0 +1,32 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.LavaFluid; +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(LavaFluid.class) +public abstract class LavaFluid_renewableDeepslateMixin { + @Shadow protected abstract void fizz(LevelAccessor world, BlockPos pos); + + @Inject(method = "spreadTo", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/Block;defaultBlockState()Lnet/minecraft/world/level/block/state/BlockState;"), cancellable = true) + private void generateDeepslate(LevelAccessor world, BlockPos pos, BlockState state, Direction direction, FluidState fluidState, CallbackInfo ci) + { + if(CarpetSettings.renewableDeepslate && ((Level)world).dimension() == Level.OVERWORLD && pos.getY() < 0) + { + world.setBlock(pos, Blocks.DEEPSLATE.defaultBlockState(), 3); + this.fizz(world, pos); + ci.cancel(); + } + } +} diff --git a/src/main/java/carpet/mixins/LayerLightEngine_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/LayerLightEngine_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..a4e7c6a --- /dev/null +++ b/src/main/java/carpet/mixins/LayerLightEngine_scarpetChunkCreationMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import carpet.fakes.Lighting_scarpetChunkCreationInterface; +import net.minecraft.world.level.lighting.LightEngine; +import net.minecraft.world.level.lighting.LayerLightSectionStorage; + +@Mixin(LightEngine.class) +public abstract class LayerLightEngine_scarpetChunkCreationMixin implements Lighting_scarpetChunkCreationInterface +{ + @Shadow + @Final + protected LayerLightSectionStorage storage; + + @Override + public void removeLightData(final long pos) + { + ((Lighting_scarpetChunkCreationInterface) this.storage).removeLightData(pos); + } +} diff --git a/src/main/java/carpet/mixins/LayerLightSectionStorage_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/LayerLightSectionStorage_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..7f41c73 --- /dev/null +++ b/src/main/java/carpet/mixins/LayerLightSectionStorage_scarpetChunkCreationMixin.java @@ -0,0 +1,56 @@ +package carpet.mixins; + +import java.util.Arrays; + +import carpet.fakes.Lighting_scarpetChunkCreationInterface; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.lighting.DataLayerStorageMap; +import net.minecraft.world.level.lighting.LayerLightSectionStorage; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.LongSet; + +@Mixin(LayerLightSectionStorage.class) +public abstract class LayerLightSectionStorage_scarpetChunkCreationMixin implements Lighting_scarpetChunkCreationInterface +{ + @Shadow + protected abstract DataLayer getDataLayer(long sectionPos, boolean cached); + + @Shadow + @Final + protected LongSet changedSections; + + @Shadow + @Final + protected DataLayerStorageMap updatingSectionData; + + @Shadow protected abstract boolean storingLightForSection(long sectionPos); + + @Shadow + @Final + protected Long2ObjectMap queuedSections; + + @Override + public void removeLightData(long cPos) + { + + for (int y = -1; y < 17; ++y) + { + long sectionPos = SectionPos.asLong(SectionPos.x(cPos), y, SectionPos.z(cPos)); + + this.queuedSections.remove(sectionPos); + + if (this.storingLightForSection(sectionPos)) + { + if (this.changedSections.add(sectionPos)) + this.updatingSectionData.copyDataLayer(sectionPos); + + Arrays.fill(this.getDataLayer(sectionPos, true).getData(), (byte) 0); + } + } + } +} diff --git a/src/main/java/carpet/mixins/LevelChunk_fillUpdatesMixin.java b/src/main/java/carpet/mixins/LevelChunk_fillUpdatesMixin.java new file mode 100644 index 0000000..52b345a --- /dev/null +++ b/src/main/java/carpet/mixins/LevelChunk_fillUpdatesMixin.java @@ -0,0 +1,44 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(LevelChunk.class) +public class LevelChunk_fillUpdatesMixin +{ + // todo onStateReplaced needs a bit more love since it removes be which is needed + @Redirect(method = "setBlockState", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;onPlace(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Z)V" + )) + private void onAdded(BlockState blockState, Level world_1, BlockPos blockPos_1, BlockState blockState_1, boolean boolean_1) + { + if (!CarpetSettings.impendingFillSkipUpdates.get()) + blockState.onPlace(world_1, blockPos_1, blockState_1, boolean_1); + } + + @Redirect(method = "setBlockState", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;onRemove(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Z)V" + )) + private void onRemovedBlock(BlockState blockState, Level world, BlockPos pos, BlockState state, boolean moved) + { + if (CarpetSettings.impendingFillSkipUpdates.get()) // doing due dilligence from AbstractBlock onStateReplaced + { + if (blockState.hasBlockEntity() && !blockState.is(state.getBlock())) + { + world.removeBlockEntity(pos); + } + } + else + { + blockState.onRemove(world, pos, state, moved); + } + } +} diff --git a/src/main/java/carpet/mixins/LevelChunk_movableBEMixin.java b/src/main/java/carpet/mixins/LevelChunk_movableBEMixin.java new file mode 100644 index 0000000..9ea4430 --- /dev/null +++ b/src/main/java/carpet/mixins/LevelChunk_movableBEMixin.java @@ -0,0 +1,162 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.WorldChunkInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.piston.MovingPistonBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import org.jetbrains.annotations.Nullable; +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(LevelChunk.class) +public abstract class LevelChunk_movableBEMixin extends ChunkAccess implements WorldChunkInterface +{ + @Shadow + @Final + Level level; + + public LevelChunk_movableBEMixin(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biome, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable BlendingData blendingData) { + super(pos, upgradeData, heightLimitView, biome, inhabitedTime, sectionArrayInitializer, blendingData); + } + + @Shadow + /* @Nullable */ + public abstract BlockEntity getBlockEntity(BlockPos blockPos_1, LevelChunk.EntityCreationType worldChunk$CreationType_1); + + @Shadow protected abstract void updateBlockEntityTicker(T blockEntity); + + @Shadow public abstract void addAndRegisterBlockEntity(BlockEntity blockEntity); + + // Fix Failure: If a moving BlockEntity is placed while BlockEntities are ticking, this will not find it and then replace it with a new TileEntity! + // blockEntity_2 = this.getBlockEntity(blockPos_1, WorldChunk.CreationType.CHECK); + // question is - with the changes in the BE handling this might not be a case anymore + @Redirect(method = "setBlockState", at = @At(value = "INVOKE", ordinal = 0, + target = "Lnet/minecraft/world/level/chunk/LevelChunk;getBlockEntity(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/chunk/LevelChunk$EntityCreationType;)Lnet/minecraft/world/level/block/entity/BlockEntity;")) + private BlockEntity ifGetBlockEntity(LevelChunk worldChunk, BlockPos blockPos_1, + LevelChunk.EntityCreationType worldChunk$CreationType_1) + { + if (!CarpetSettings.movableBlockEntities) + { + return this.getBlockEntity(blockPos_1, LevelChunk.EntityCreationType.CHECK); + } + else + { + return this.level.getBlockEntity(blockPos_1); + } + } + + + /** + * Sets the Blockstate and the BlockEntity. + * Only sets BlockEntity if Block is BlockEntityProvider, but doesn't check if it actually matches (e.g. can assign beacon to chest entity). + * + * @author 2No2Name + */ + /* @Nullable */ + // todo update me to the new version + public BlockState setBlockStateWithBlockEntity(BlockPos blockPos_1, BlockState newBlockState, BlockEntity newBlockEntity, + boolean boolean_1) + { + int x = blockPos_1.getX() & 15; + int y = blockPos_1.getY(); + int z = blockPos_1.getZ() & 15; + LevelChunkSection chunkSection = this.getSection(this.getSectionIndex(y)); + if (chunkSection.hasOnlyAir()) + { + if (newBlockState.isAir()) + { + return null; + } + } + + boolean boolean_2 = chunkSection.hasOnlyAir(); + BlockState oldBlockState = chunkSection.setBlockState(x, y & 15, z, newBlockState); + if (oldBlockState == newBlockState) + { + return null; + } + else + { + Block newBlock = newBlockState.getBlock(); + Block oldBlock = oldBlockState.getBlock(); + ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING)).update(x, y, z, newBlockState); + ((Heightmap) this.heightmaps.get(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES)).update(x, y, z, newBlockState); + ((Heightmap) this.heightmaps.get(Heightmap.Types.OCEAN_FLOOR)).update(x, y, z, newBlockState); + ((Heightmap) this.heightmaps.get(Heightmap.Types.WORLD_SURFACE)).update(x, y, z, newBlockState); + boolean boolean_3 = chunkSection.hasOnlyAir(); + if (boolean_2 != boolean_3) + { + this.level.getChunkSource().getLightEngine().updateSectionStatus(blockPos_1, boolean_3); + } + + if (!this.level.isClientSide) + { + if (!(oldBlock instanceof MovingPistonBlock))//this is a movableTE special case, if condition wasn't there it would remove the blockentity that was carried for some reason + oldBlockState.onRemove(this.level, blockPos_1, newBlockState, boolean_1);//this kills it + } + else if (oldBlock != newBlock && oldBlock instanceof EntityBlock) + { + this.level.removeBlockEntity(blockPos_1); + } + + if (chunkSection.getBlockState(x, y & 15, z).getBlock() != newBlock) + { + return null; + } + else + { + BlockEntity oldBlockEntity = null; + if (oldBlockState.hasBlockEntity()) + { + oldBlockEntity = this.getBlockEntity(blockPos_1, LevelChunk.EntityCreationType.CHECK); + if (oldBlockEntity != null) + { + oldBlockEntity.setBlockState(oldBlockState); + updateBlockEntityTicker(oldBlockEntity); + } + } + + if (oldBlockState.hasBlockEntity()) + { + if (newBlockEntity == null) + { + newBlockEntity = ((EntityBlock) newBlock).newBlockEntity(blockPos_1, newBlockState); + } + if (newBlockEntity != oldBlockEntity && newBlockEntity != null) + { + newBlockEntity.clearRemoved(); + this.level.setBlockEntity(newBlockEntity); + newBlockEntity.setBlockState(newBlockState); + updateBlockEntityTicker(newBlockEntity); + } + } + + if (!this.level.isClientSide) + { + newBlockState.onPlace(this.level, blockPos_1, oldBlockState, boolean_1); //This can call setblockstate! (e.g. hopper does) + } + + this.unsaved = true; // shouldSave + return oldBlockState; + } + } + } +} diff --git a/src/main/java/carpet/mixins/LevelEntityGetterAdapter_scarpetMixin.java b/src/main/java/carpet/mixins/LevelEntityGetterAdapter_scarpetMixin.java new file mode 100644 index 0000000..51ae921 --- /dev/null +++ b/src/main/java/carpet/mixins/LevelEntityGetterAdapter_scarpetMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.SimpleEntityLookupInterface; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; +import java.util.stream.Collectors; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntitySectionStorage; +import net.minecraft.world.level.entity.LevelEntityGetterAdapter; + +@Mixin(LevelEntityGetterAdapter.class) +public class LevelEntityGetterAdapter_scarpetMixin implements SimpleEntityLookupInterface +{ + + @Shadow @Final private EntitySectionStorage sectionStorage; + + @Override + public List getChunkEntities(ChunkPos chpos) { + return this.sectionStorage.getExistingSectionsInChunk(chpos.toLong()).flatMap(EntitySection::getEntities).collect(Collectors.toList()); + } +} + + diff --git a/src/main/java/carpet/mixins/LevelLightEngine_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/LevelLightEngine_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..55bb324 --- /dev/null +++ b/src/main/java/carpet/mixins/LevelLightEngine_scarpetChunkCreationMixin.java @@ -0,0 +1,31 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import carpet.fakes.Lighting_scarpetChunkCreationInterface; +import net.minecraft.world.level.lighting.LightEngine; +import net.minecraft.world.level.lighting.LevelLightEngine; + +@Mixin(LevelLightEngine.class) +public abstract class LevelLightEngine_scarpetChunkCreationMixin implements Lighting_scarpetChunkCreationInterface +{ + @Shadow + @Final + private LightEngine blockEngine; + + @Shadow + @Final + private LightEngine skyEngine; + + @Override + public void removeLightData(final long pos) + { + if (this.blockEngine != null) + ((Lighting_scarpetChunkCreationInterface) this.blockEngine).removeLightData(pos); + + if (this.skyEngine != null) + ((Lighting_scarpetChunkCreationInterface) this.skyEngine).removeLightData(pos); + } +} diff --git a/src/main/java/carpet/mixins/LevelRenderer_creativeNoClipMixin.java b/src/main/java/carpet/mixins/LevelRenderer_creativeNoClipMixin.java new file mode 100644 index 0000000..a281c7a --- /dev/null +++ b/src/main/java/carpet/mixins/LevelRenderer_creativeNoClipMixin.java @@ -0,0 +1,19 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.LevelRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(LevelRenderer.class) +public class LevelRenderer_creativeNoClipMixin +{ + @Redirect(method = "renderLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;isSpectator()Z")) + private boolean canSeeWorld(LocalPlayer clientPlayerEntity) + { + return clientPlayerEntity.isSpectator() || (CarpetSettings.creativeNoClip && clientPlayerEntity.isCreative()); + } + +} diff --git a/src/main/java/carpet/mixins/LevelRenderer_fogOffMixin.java b/src/main/java/carpet/mixins/LevelRenderer_fogOffMixin.java new file mode 100644 index 0000000..d754a8e --- /dev/null +++ b/src/main/java/carpet/mixins/LevelRenderer_fogOffMixin.java @@ -0,0 +1,24 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.client.renderer.DimensionSpecialEffects; +import net.minecraft.client.renderer.LevelRenderer; +//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 = LevelRenderer.class, priority = 69420) +public class LevelRenderer_fogOffMixin +{ + @Redirect(method = "renderLevel", require = 0, expect = 0, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/DimensionSpecialEffects;isFoggyAt(II)Z" + )) + private boolean isReallyThick(DimensionSpecialEffects skyProperties, int x, int z) + { + if (CarpetSettings.fogOff) return false; + return skyProperties.isFoggyAt(x, z); + } + +} diff --git a/src/main/java/carpet/mixins/LevelRenderer_scarpetRenderMixin.java b/src/main/java/carpet/mixins/LevelRenderer_scarpetRenderMixin.java new file mode 100644 index 0000000..f2a71f2 --- /dev/null +++ b/src/main/java/carpet/mixins/LevelRenderer_scarpetRenderMixin.java @@ -0,0 +1,50 @@ +package carpet.mixins; + +import carpet.network.CarpetClient; +import carpet.script.utils.ShapesRenderer; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.RenderBuffers; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import org.joml.Matrix4f; +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(LevelRenderer.class) +public class LevelRenderer_scarpetRenderMixin +{ + @Inject(method = "", at = @At("RETURN")) + private void addRenderers(Minecraft minecraft, EntityRenderDispatcher entityRenderDispatcher, BlockEntityRenderDispatcher blockEntityRenderDispatcher, RenderBuffers renderBuffers, CallbackInfo ci) + { + CarpetClient.shapes = new ShapesRenderer(minecraft); + } + + @Inject(method = "renderLevel", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/Sheets;translucentCullBlockSheet()Lnet/minecraft/client/renderer/RenderType;", shift = At.Shift.BEFORE + //target = "Lnet/minecraft/client/render/RenderLayer;getWaterMask()Lnet/minecraft/client/render/RenderLayer;", shift = At.Shift.AFTER + //target = "Lnet/minecraft/client/render/BufferBuilderStorage;getEffectVertexConsumers()Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;", shift = At.Shift.BEFORE + //target = "Lnet/minecraft/client/render/WorldRenderer;renderChunkDebugInfo(Lnet/minecraft/client/render/Camera;)V", shift = At.Shift.AFTER + //target = "Lnet/minecraft/client/render/BackgroundRenderer;method_23792()V", shift = At.Shift.AFTER + //target = "Lnet/minecraft/client/render/BufferBuilderStorage;getEntityVertexConsumers()Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;", shift = At.Shift.AFTER + //target = "Lnet/minecraft/client/render/WorldRenderer;renderChunkDebugInfo(Lnet/minecraft/client/render/Camera;)V", shift = At.Shift.AFTER // before return + )) + private void renderScarpetThings(final DeltaTracker deltaTracker, final boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightTexture lightmapTextureManager, Matrix4f modelViewMatrix, Matrix4f matrix4f, CallbackInfo ci) + { + // in normal circumstances we want to render shapes at the very end so it appears correctly behind stuff. + // we might actually not need to play with render hooks here. + //if (!FabricAPIHooks.WORLD_RENDER_EVENTS && CarpetClient.shapes != null ) + if (CarpetClient.shapes != null) + { + CarpetClient.shapes.render(modelViewMatrix, camera, deltaTracker.getGameTimeDeltaPartialTick(false)); + } + } +} diff --git a/src/main/java/carpet/mixins/Level_fillUpdatesMixin.java b/src/main/java/carpet/mixins/Level_fillUpdatesMixin.java new file mode 100644 index 0000000..cb050c7 --- /dev/null +++ b/src/main/java/carpet/mixins/Level_fillUpdatesMixin.java @@ -0,0 +1,33 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Level.class) +public abstract class Level_fillUpdatesMixin +{ + @ModifyConstant(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z", //setBlockState main + constant = @Constant(intValue = 16)) + private int addFillUpdatesInt(int original) { + if (CarpetSettings.impendingFillSkipUpdates.get()) + return -1; + return original; + } + + @Redirect(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z", at = @At( //setBlockState main + value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;blockUpdated(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;)V" + )) + private void updateNeighborsMaybe(Level world, BlockPos blockPos, Block block) + { + if (!CarpetSettings.impendingFillSkipUpdates.get()) world.blockUpdated(blockPos, block); + } + +} diff --git a/src/main/java/carpet/mixins/Level_getOtherEntitiesLimited.java b/src/main/java/carpet/mixins/Level_getOtherEntitiesLimited.java new file mode 100644 index 0000000..4a6b4a7 --- /dev/null +++ b/src/main/java/carpet/mixins/Level_getOtherEntitiesLimited.java @@ -0,0 +1,63 @@ +package carpet.mixins; + +import carpet.fakes.LevelInterface; +import com.google.common.collect.Lists; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.phys.AABB; + +@Mixin(Level.class) +public abstract class Level_getOtherEntitiesLimited implements LevelInterface { + + private static final RuntimeException CONTROL_FLOW_EXCEPTION = new RuntimeException("Should be caught for control flow in World_getOtherEntitiesLimited!"); + + @Override + public List getOtherEntitiesLimited(@Nullable Entity except, AABB box, Predicate predicate, int limit) { + this.getProfiler().incrementCounter("getEntities"); // visit + AtomicInteger checkedEntities = new AtomicInteger(); + List list = Lists.newArrayList(); + try { + this.getEntities().get(box, (entity) -> { + if (checkedEntities.getAndIncrement() > limit) { + throw CONTROL_FLOW_EXCEPTION; + } + + if (entity != except && predicate.test(entity)) { + list.add(entity); + } + + if (entity instanceof EnderDragon) { + EnderDragonPart[] var4 = ((EnderDragon) entity).getSubEntities(); + + for (EnderDragonPart enderDragonPart : var4) { + if (entity != except && predicate.test(enderDragonPart)) { + list.add(enderDragonPart); + } + } + } + }); + } catch (RuntimeException e) { + if (e != CONTROL_FLOW_EXCEPTION) + // If it wasn't the exception we were watching, rethrow it + throw e; + } + return list; + } + + @Shadow + public abstract ProfilerFiller getProfiler(); + + @Shadow + protected abstract LevelEntityGetter getEntities(); +} diff --git a/src/main/java/carpet/mixins/Level_movableBEMixin.java b/src/main/java/carpet/mixins/Level_movableBEMixin.java new file mode 100644 index 0000000..d582036 --- /dev/null +++ b/src/main/java/carpet/mixins/Level_movableBEMixin.java @@ -0,0 +1,130 @@ +package carpet.mixins; + +import carpet.fakes.WorldChunkInterface; +import carpet.fakes.LevelInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.FullChunkStatus; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.LidBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(Level.class) +public abstract class Level_movableBEMixin implements LevelInterface, LevelAccessor +{ + @Shadow + @Final + public boolean isClientSide; + + @Shadow + public abstract LevelChunk getChunkAt(BlockPos blockPos_1); + + @Shadow + public abstract BlockState getBlockState(BlockPos blockPos_1); + + //@Shadow + //public abstract ChunkManager getChunkManager(); + + @Shadow + public abstract void setBlocksDirty(BlockPos blockPos_1, BlockState s1, BlockState s2); + + @Shadow + public abstract void sendBlockUpdated(BlockPos var1, BlockState var2, BlockState var3, int var4); + + @Shadow + public abstract void updateNeighborsAt(BlockPos blockPos_1, Block block_1); + + @Shadow + public abstract void onBlockStateChange(BlockPos blockPos_1, BlockState blockState_1, BlockState blockState_2); + + @Shadow public abstract ProfilerFiller getProfiler(); + + @Shadow public abstract void updateNeighbourForOutputSignal(BlockPos pos, Block block); + + //@Shadow public abstract boolean setBlockState(BlockPos pos, BlockState state, int flags); + + @Shadow public abstract boolean isDebug(); + + /** + * @author 2No2Name + */ + @Override + public boolean setBlockStateWithBlockEntity(BlockPos blockPos_1, BlockState blockState_1, BlockEntity newBlockEntity, int int_1) + { + if (isOutsideBuildHeight(blockPos_1) || !this.isClientSide && isDebug()) return false; + LevelChunk worldChunk_1 = this.getChunkAt(blockPos_1); + Block block_1 = blockState_1.getBlock(); + + BlockState blockState_2; + if (newBlockEntity != null && block_1 instanceof EntityBlock) + { + blockState_2 = ((WorldChunkInterface) worldChunk_1).setBlockStateWithBlockEntity(blockPos_1, blockState_1, newBlockEntity, (int_1 & 64) != 0); + if (newBlockEntity instanceof LidBlockEntity) + { + scheduleTick(blockPos_1, block_1, 5); + } + } + else + { + blockState_2 = worldChunk_1.setBlockState(blockPos_1, blockState_1, (int_1 & 64) != 0); + } + + if (blockState_2 == null) + { + return false; + } + else + { + BlockState blockState_3 = this.getBlockState(blockPos_1); + + if (blockState_3 != blockState_2 && (blockState_3.getLightBlock((BlockGetter) this, blockPos_1) != blockState_2.getLightBlock((BlockGetter) this, blockPos_1) || blockState_3.getLightEmission() != blockState_2.getLightEmission() || blockState_3.useShapeForLightOcclusion() || blockState_2.useShapeForLightOcclusion())) + { + ProfilerFiller profiler = getProfiler(); + profiler.push("queueCheckLight"); + this.getChunkSource().getLightEngine().checkBlock(blockPos_1); + profiler.pop(); + } + + if (blockState_3 == blockState_1) + { + if (blockState_2 != blockState_3) + { + this.setBlocksDirty(blockPos_1, blockState_2, blockState_3); + } + + if ((int_1 & 2) != 0 && (!this.isClientSide || (int_1 & 4) == 0) && (this.isClientSide || worldChunk_1.getFullStatus() != null && worldChunk_1.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING))) + { + this.sendBlockUpdated(blockPos_1, blockState_2, blockState_1, int_1); + } + + if (!this.isClientSide && (int_1 & 1) != 0) + { + this.updateNeighborsAt(blockPos_1, blockState_2.getBlock()); + if (blockState_1.hasAnalogOutputSignal()) + { + updateNeighbourForOutputSignal(blockPos_1, block_1); + } + } + + if ((int_1 & 16) == 0) + { + int int_2 = int_1 & -34; + blockState_2.updateIndirectNeighbourShapes(this, blockPos_1, int_2); // prepare + blockState_1.updateNeighbourShapes(this, blockPos_1, int_2); // updateNeighbours + blockState_1.updateIndirectNeighbourShapes(this, blockPos_1, int_2); // prepare + } + this.onBlockStateChange(blockPos_1, blockState_2, blockState_3); + } + return true; + } + } +} diff --git a/src/main/java/carpet/mixins/Level_scarpetPlopMixin.java b/src/main/java/carpet/mixins/Level_scarpetPlopMixin.java new file mode 100644 index 0000000..897587d --- /dev/null +++ b/src/main/java/carpet/mixins/Level_scarpetPlopMixin.java @@ -0,0 +1,31 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.Heightmap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Level.class) +public class Level_scarpetPlopMixin +{ + + @Redirect(method = "getHeight", at = @At( + value = "INVOKE", + target = "net/minecraft/world/level/chunk/LevelChunk.getHeight(Lnet/minecraft/world/level/levelgen/Heightmap$Types;II)I" + )) + private int fixSampleHeightmap(LevelChunk chunk, Heightmap.Types type, int x, int z) + { + if (CarpetSettings.skipGenerationChecks.get()) + { + Heightmap.Types newType = type; + if (type == Heightmap.Types.OCEAN_FLOOR_WG) newType = Heightmap.Types.OCEAN_FLOOR; + else if (type == Heightmap.Types.WORLD_SURFACE_WG) newType = Heightmap.Types.WORLD_SURFACE; + return chunk.getHeight(newType, x, z); + } + return chunk.getHeight(type, x, z); + } +} + diff --git a/src/main/java/carpet/mixins/Level_tickMixin.java b/src/main/java/carpet/mixins/Level_tickMixin.java new file mode 100644 index 0000000..6340237 --- /dev/null +++ b/src/main/java/carpet/mixins/Level_tickMixin.java @@ -0,0 +1,65 @@ +package carpet.mixins; + +import carpet.fakes.LevelInterface; +import carpet.utils.CarpetProfiler; +import net.minecraft.world.level.redstone.NeighborUpdater; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +@Mixin(Level.class) +public abstract class Level_tickMixin implements LevelInterface +{ + @Shadow @Final public boolean isClientSide; + @Shadow @Final protected NeighborUpdater neighborUpdater; + CarpetProfiler.ProfilerToken currentSection; + CarpetProfiler.ProfilerToken entitySection; + + Map, Entity> precookedMobs = new HashMap<>(); + + @Override + @Unique + public NeighborUpdater getNeighborUpdater() { + return this.neighborUpdater; + } + + @Override + public Map, Entity> getPrecookedMobs() + { + return precookedMobs; + } + + @Inject(method = "tickBlockEntities", at = @At("HEAD")) + private void startBlockEntities(CallbackInfo ci) { + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Block Entities", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tickBlockEntities", at = @At("TAIL")) + private void endBlockEntities(CallbackInfo ci) { + CarpetProfiler.end_current_section(currentSection); + } + + @Inject(method = "guardEntityTick", at = @At("HEAD"), cancellable = true) + private void startEntity(Consumer consumer_1, Entity e, CallbackInfo ci) + { + entitySection = CarpetProfiler.start_entity_section((Level) (Object) this, e, CarpetProfiler.TYPE.ENTITY); + } + + @Inject(method = "guardEntityTick", at = @At("TAIL")) + private void endEntity(Consumer call, Entity e, CallbackInfo ci) { + CarpetProfiler.end_current_entity_section(entitySection); + } + + +} diff --git a/src/main/java/carpet/mixins/LiquidBlock_renewableBlackstoneMixin.java b/src/main/java/carpet/mixins/LiquidBlock_renewableBlackstoneMixin.java new file mode 100644 index 0000000..8f097ff --- /dev/null +++ b/src/main/java/carpet/mixins/LiquidBlock_renewableBlackstoneMixin.java @@ -0,0 +1,47 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FlowingFluid; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LiquidBlock.class) +public abstract class LiquidBlock_renewableBlackstoneMixin +{ + @Shadow @Final protected FlowingFluid fluid; + + @Shadow protected abstract void fizz(LevelAccessor world, BlockPos pos); + + @Inject(method = "shouldSpreadLiquid", at = @At("TAIL"), cancellable = true) + private void receiveFluidToBlackstone(Level world, BlockPos pos, BlockState state, CallbackInfoReturnable cir) + { + if (CarpetSettings.renewableBlackstone) + { + if (fluid.is(FluidTags.LAVA)) { + for(Direction direction : Direction.values()) + { + if (direction != Direction.DOWN) { + BlockPos blockPos = pos.relative(direction); // offset + if (world.getBlockState(blockPos).is(Blocks.BLUE_ICE)) { + world.setBlockAndUpdate(pos, Blocks.BLACKSTONE.defaultBlockState()); + fizz(world, pos); + cir.setReturnValue(false); + } + } + } + } + } + } +} diff --git a/src/main/java/carpet/mixins/LiquidBlock_renewableDeepslateMixin.java b/src/main/java/carpet/mixins/LiquidBlock_renewableDeepslateMixin.java new file mode 100644 index 0000000..d80b85b --- /dev/null +++ b/src/main/java/carpet/mixins/LiquidBlock_renewableDeepslateMixin.java @@ -0,0 +1,32 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LiquidBlock.class) +public abstract class LiquidBlock_renewableDeepslateMixin { + + @Shadow protected abstract void fizz(LevelAccessor world, BlockPos pos); + + @Inject(method = "shouldSpreadLiquid", at = @At(value = "INVOKE",target = "Lnet/minecraft/world/level/material/FluidState;isSource()Z"), cancellable = true) + private void receiveFluidToDeepslate(Level world, BlockPos pos, BlockState state, CallbackInfoReturnable cir) + { + if(CarpetSettings.renewableDeepslate && !world.getFluidState(pos).isSource() && world.dimension() == Level.OVERWORLD && pos.getY() < 0) + { + world.setBlockAndUpdate(pos, Blocks.COBBLED_DEEPSLATE.defaultBlockState()); + this.fizz(world, pos); + cir.setReturnValue(false); + cir.cancel(); + } + } +} diff --git a/src/main/java/carpet/mixins/LivingEntity_cleanLogsMixin.java b/src/main/java/carpet/mixins/LivingEntity_cleanLogsMixin.java new file mode 100644 index 0000000..525d918 --- /dev/null +++ b/src/main/java/carpet/mixins/LivingEntity_cleanLogsMixin.java @@ -0,0 +1,27 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(LivingEntity.class) +public abstract class LivingEntity_cleanLogsMixin extends Entity +{ + + public LivingEntity_cleanLogsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Redirect(method = "die", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;hasCustomName()Z")) + private boolean shouldLogDeaths(LivingEntity livingEntity) + { + return livingEntity.hasCustomName() && CarpetSettings.cleanLogs && level().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES); + } +} 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 0000000..5b22f42 --- /dev/null +++ b/src/main/java/carpet/mixins/LivingEntity_creativeFlyMixin.java @@ -0,0 +1,59 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.patches.EntityPlayerMPFake; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +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 protected abstract float getFlyingSpeed(); + + public LivingEntity_creativeFlyMixin(EntityType type, Level 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 Player) + { + Player self = (Player)(Object)(this); + if (self.getAbilities().flying && ! onGround() ) + return (float)(1.0-CarpetSettings.creativeFlyDrag); + } + return original; + } + + + @Inject(method = "getFrictionInfluencedSpeed(F)F", at = @At("HEAD"), cancellable = true) + private void flyingAltSpeed(float slipperiness, CallbackInfoReturnable cir) + { + if (CarpetSettings.creativeFlySpeed != 1.0D && (Object)this instanceof Player) + { + Player self = (Player)(Object)(this); + if (self.getAbilities().flying && !onGround()) + cir.setReturnValue( getFlyingSpeed() * (float)CarpetSettings.creativeFlySpeed); + } + } + + @Inject(method = "canUsePortal", at = @At("HEAD"), cancellable = true) + private void canChangeDimensions(CallbackInfoReturnable cir) + { + if (CarpetSettings.isCreativeFlying(this)) { + cir.setReturnValue(false); + } + } +} diff --git a/src/main/java/carpet/mixins/LivingEntity_maxCollisionsMixin.java b/src/main/java/carpet/mixins/LivingEntity_maxCollisionsMixin.java new file mode 100644 index 0000000..f7168f9 --- /dev/null +++ b/src/main/java/carpet/mixins/LivingEntity_maxCollisionsMixin.java @@ -0,0 +1,86 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.LevelInterface; +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; + +import java.util.List; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; + +@Mixin(LivingEntity.class) +public abstract class LivingEntity_maxCollisionsMixin extends Entity +{ + + public LivingEntity_maxCollisionsMixin(EntityType entityType_1, Level world_1) + { + super(entityType_1, world_1); + } + + @Shadow protected abstract void doPush(Entity entity_1); + + @Inject(method = "pushEntities", cancellable = true, at = @At("HEAD")) + private void tickPushingReplacement(CallbackInfo ci) { + if (CarpetSettings.maxEntityCollisions == 0) + { + return; + } + List entities; + int maxEntityCramming =-1; + if (CarpetSettings.maxEntityCollisions > 0) + { + maxEntityCramming = this.level().getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING); + entities = ((LevelInterface) this.level()).getOtherEntitiesLimited( + this, + this.getBoundingBox(), + EntitySelector.pushableBy(this), + Math.max(CarpetSettings.maxEntityCollisions, maxEntityCramming)); + } + else + { + entities = this.level().getEntities(this, this.getBoundingBox(), EntitySelector.pushableBy(this)); + } + + if (!entities.isEmpty()) { + if (maxEntityCramming < 0) maxEntityCramming = this.level().getGameRules().getInt(GameRules.RULE_MAX_ENTITY_CRAMMING); + if (maxEntityCramming > 0 && entities.size() > maxEntityCramming - 1 && this.random.nextInt(4) == 0) { + int candidates = 0; + + for (Entity entity : entities) { + if (!entity.isPassenger()) { + ++candidates; + } + } + + if (candidates > maxEntityCramming - 1) { + this.hurt(damageSources().cramming(), 6.0F); + } + } + if (CarpetSettings.maxEntityCollisions > 0 && entities.size() > CarpetSettings.maxEntityCollisions) + { + for (Entity entity : entities.subList(0, CarpetSettings.maxEntityCollisions)) + { + this.doPush(entity); + } + } + else + { + for (Entity entity : entities) + { + this.doPush(entity); + } + } + } + ci.cancel(); + } + + +} diff --git a/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java b/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java new file mode 100644 index 0000000..fe46142 --- /dev/null +++ b/src/main/java/carpet/mixins/LivingEntity_scarpetEventsMixin.java @@ -0,0 +1,72 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.fakes.LivingEntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +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; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import static carpet.script.CarpetEventServer.Event.PLAYER_DEALS_DAMAGE; + +@Mixin(LivingEntity.class) +public abstract class LivingEntity_scarpetEventsMixin extends Entity implements LivingEntityInterface +{ + + @Shadow protected abstract void jumpFromGround(); + + @Shadow protected boolean jumping; + + public LivingEntity_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "die", at = @At("HEAD")) + private void onDeathCall(DamageSource damageSource_1, CallbackInfo ci) + { + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_DEATH, damageSource_1.getMsgId()); + } + + @Inject(method = "actuallyHurt", cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/LivingEntity;getDamageAfterArmorAbsorb(Lnet/minecraft/world/damagesource/DamageSource;F)F", + shift = At.Shift.BEFORE + )) + private void entityTakingDamage(DamageSource source, float amount, CallbackInfo ci) + { + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_DAMAGE, amount, source); + // this is not applicable since its not a playr for sure + //if (entity instanceof ServerPlayerEntity && PLAYER_TAKES_DAMAGE.isNeeded()) + //{ + // PLAYER_TAKES_DAMAGE.onDamage(entity, float_2, damageSource_1); + //} + if (source.getEntity() instanceof ServerPlayer && PLAYER_DEALS_DAMAGE.isNeeded()) + { + if(PLAYER_DEALS_DAMAGE.onDamage(this, amount, source)) { + ci.cancel(); + } + } + } + + @Override + public void doJumpCM() + { + jumpFromGround(); + } + + @Override + public boolean isJumpingCM() + { + return jumping; + } +} diff --git a/src/main/java/carpet/mixins/MerchantResultSlot_scarpetEventMixin.java b/src/main/java/carpet/mixins/MerchantResultSlot_scarpetEventMixin.java new file mode 100644 index 0000000..0ebb016 --- /dev/null +++ b/src/main/java/carpet/mixins/MerchantResultSlot_scarpetEventMixin.java @@ -0,0 +1,34 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static carpet.script.CarpetEventServer.Event.PLAYER_TRADES; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.MerchantContainer; +import net.minecraft.world.inventory.MerchantResultSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.Merchant; + +@Mixin(MerchantResultSlot.class) +public abstract class MerchantResultSlot_scarpetEventMixin { + @Shadow @Final private Merchant merchant; + + @Shadow @Final private MerchantContainer slots; + + @Inject(method = "onTake", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/item/trading/Merchant;notifyTrade(Lnet/minecraft/world/item/trading/MerchantOffer;)V") + ) + private void onTrade(Player player, ItemStack stack, CallbackInfo ci) { + if(PLAYER_TRADES.isNeeded() && !player.level().isClientSide()) + { + PLAYER_TRADES.onTrade((ServerPlayer) player, merchant, slots.getActiveOffer()); + } + } +} diff --git a/src/main/java/carpet/mixins/MinecraftMixin.java b/src/main/java/carpet/mixins/MinecraftMixin.java new file mode 100644 index 0000000..ffe95fb --- /dev/null +++ b/src/main/java/carpet/mixins/MinecraftMixin.java @@ -0,0 +1,33 @@ +package carpet.mixins; + +import carpet.network.CarpetClient; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.multiplayer.ClientLevel; +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(Minecraft.class) +public class MinecraftMixin +{ + @Shadow public ClientLevel level; + + @Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At("HEAD")) + private void onCloseGame(Screen screen, CallbackInfo ci) + { + CarpetClient.disconnect(); + } + + @Inject(at = @At("HEAD"), method = "tick") + private void onClientTick(CallbackInfo info) { + if (this.level != null) { + boolean runsNormally = level.tickRateManager().runsNormally(); + // hope server doesn't need to tick - should be handled by the server on its own + if (!runsNormally) + CarpetClient.shapes.renewShapes(); + } + } +} diff --git a/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java b/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java new file mode 100644 index 0000000..b227c40 --- /dev/null +++ b/src/main/java/carpet/mixins/MinecraftServer_coreMixin.java @@ -0,0 +1,61 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import carpet.utils.CarpetProfiler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +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; + +import java.util.function.BooleanSupplier; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServer_coreMixin +{ + //to inject right before + // this.tickWorlds(booleanSupplier_1); + @Inject( + method = "tickServer", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;tickChildren(Ljava/util/function/BooleanSupplier;)V", + shift = At.Shift.BEFORE, + ordinal = 0 + ) + ) + private void onTick(BooleanSupplier booleanSupplier_1, CallbackInfo ci) { + CarpetProfiler.ProfilerToken token = CarpetProfiler.start_section(null, "Carpet", CarpetProfiler.TYPE.GENERAL); + CarpetServer.tick((MinecraftServer) (Object) this); + CarpetProfiler.end_current_section(token); + } + + @Inject(method = "loadLevel", at = @At("HEAD")) + private void serverLoaded(CallbackInfo ci) + { + CarpetServer.onServerLoaded((MinecraftServer) (Object) this); + } + + @Inject(method = "loadLevel", at = @At("RETURN")) + private void serverLoadedWorlds(CallbackInfo ci) + { + CarpetServer.onServerLoadedWorlds((MinecraftServer) (Object) this); + } + + @Inject(method = "stopServer", at = @At("HEAD")) + private void serverClosed(CallbackInfo ci) + { + CarpetServer.onServerClosed((MinecraftServer) (Object) this); + } + + @Inject(method = "stopServer", at = @At("TAIL")) + private void serverDoneClosed(CallbackInfo ci) + { + CarpetServer.onServerDoneClosing((MinecraftServer) (Object) this); + } + + @Shadow + public abstract ServerLevel overworld(); +} diff --git a/src/main/java/carpet/mixins/MinecraftServer_pingPlayerSampleLimit.java b/src/main/java/carpet/mixins/MinecraftServer_pingPlayerSampleLimit.java new file mode 100644 index 0000000..e53249c --- /dev/null +++ b/src/main/java/carpet/mixins/MinecraftServer_pingPlayerSampleLimit.java @@ -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 = "tickServer", constant = @Constant(intValue = 12), require = 0, allow = 1) + private int modifyPlayerSampleLimit(int value) + { + return CarpetSettings.pingPlayerListLimit; + } +} diff --git a/src/main/java/carpet/mixins/MinecraftServer_scarpetMixin.java b/src/main/java/carpet/mixins/MinecraftServer_scarpetMixin.java new file mode 100644 index 0000000..a52d55a --- /dev/null +++ b/src/main/java/carpet/mixins/MinecraftServer_scarpetMixin.java @@ -0,0 +1,129 @@ +package carpet.mixins; + +import carpet.fakes.MinecraftServerInterface; +import carpet.script.CarpetScriptServer; +import net.minecraft.Util; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerFunctionManager; +import net.minecraft.server.ServerTickRateManager; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.players.PlayerList; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; +import java.util.function.BooleanSupplier; + +import static carpet.script.CarpetEventServer.Event.ENDER_TICK; +import static carpet.script.CarpetEventServer.Event.NETHER_TICK; +import static carpet.script.CarpetEventServer.Event.TICK; + +@Mixin(MinecraftServer.class) +public abstract class MinecraftServer_scarpetMixin extends ReentrantBlockableEventLoop implements MinecraftServerInterface +{ + private CarpetScriptServer scriptServer; + + public MinecraftServer_scarpetMixin(String string_1) + { + super(string_1); + } + + @Shadow protected abstract void tickServer(BooleanSupplier booleanSupplier_1); + + @Shadow @Final protected LevelStorageSource.LevelStorageAccess storageSource; + + @Shadow @Final private Map, ServerLevel> levels; + + //@Shadow private ServerResources resources; + + @Shadow private MinecraftServer.ReloadableResources resources; + + @Shadow public abstract RegistryAccess.Frozen registryAccess(); + + @Shadow public abstract PlayerList getPlayerList(); + + @Shadow @Final private ServerFunctionManager functionManager; + + @Shadow @Final private StructureTemplateManager structureTemplateManager; + + @Shadow public abstract ServerTickRateManager tickRateManager(); + + @Shadow private long nextTickTimeNanos; + + @Shadow private long lastOverloadWarningNanos; + + @Override + public void forceTick(BooleanSupplier isAhead) + { + nextTickTimeNanos = lastOverloadWarningNanos = Util.getNanos(); + tickServer(isAhead); + pollTask(); + while(pollTask()) {Thread.yield();} + } + + @Override + public LevelStorageSource.LevelStorageAccess getCMSession() + { + return storageSource; + } + + @Override + public Map, ServerLevel> getCMWorlds() + { + return levels; + } + + @Inject(method = "tickServer", at = @At( + value = "CONSTANT", + args = "stringValue=tallying" + )) + public void tickTasks(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + if (!tickRateManager().runsNormally()) + { + return; + } + TICK.onTick((MinecraftServer) (Object) this); + NETHER_TICK.onTick((MinecraftServer) (Object) this); + ENDER_TICK.onTick((MinecraftServer) (Object) this); + } + + @Override + public void reloadAfterReload(RegistryAccess newRegs) + { + resources.managers().updateRegistryTags(); + getPlayerList().saveAll(); + getPlayerList().reloadResources(); + functionManager.replaceLibrary(this.resources.managers().getFunctionLibrary()); + structureTemplateManager.onResourceManagerReload(this.resources.resourceManager()); + } + + @Override + public MinecraftServer.ReloadableResources getResourceManager() + { + return resources; + } + + @Override + public void addScriptServer(final CarpetScriptServer scriptServer) + { + this.scriptServer = scriptServer; + } + + @Override + public CarpetScriptServer getScriptServer() + { + return scriptServer; + } +} diff --git a/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java b/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java new file mode 100644 index 0000000..27fde78 --- /dev/null +++ b/src/main/java/carpet/mixins/MinecraftServer_tickspeedMixin.java @@ -0,0 +1,99 @@ +package carpet.mixins; + +import carpet.fakes.MinecraftServerInterface; +import carpet.utils.CarpetProfiler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.util.thread.ReentrantBlockableEventLoop; +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; + +import java.util.function.BooleanSupplier; + +@Mixin(value = MinecraftServer.class, priority = Integer.MAX_VALUE - 10) +public abstract class MinecraftServer_tickspeedMixin extends ReentrantBlockableEventLoop implements MinecraftServerInterface +{ + public MinecraftServer_tickspeedMixin(String name) + { + super(name); + } + + CarpetProfiler.ProfilerToken currentSection; + + // Replaced the above cancelled while statement with this one + // could possibly just inject that mspt selection at the beginning of the loop, but then adding all mspt's to + // replace 50L will be a hassle + @Inject(method = "runServer", at = @At(value = "INVOKE", shift = At.Shift.AFTER, + target = "Lnet/minecraft/server/MinecraftServer;startMetricsRecordingTick()V")) + private void modifiedRunLoop(CallbackInfo ci) + { + if (CarpetProfiler.tick_health_requested != 0L) + { + CarpetProfiler.start_tick_profiling(); + } + } + + + @Inject(method = "tickServer", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;saveEverything(ZZZ)Z", // save + shift = At.Shift.BEFORE + )) + private void startAutosave(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(null, "Autosave", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tickServer", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;saveEverything(ZZZ)Z", + shift = At.Shift.AFTER + )) + private void finishAutosave(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + CarpetProfiler.end_current_section(currentSection); + } + + @Inject(method = "tickChildren", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;getConnection()Lnet/minecraft/server/network/ServerConnectionListener;", + shift = At.Shift.BEFORE + )) + private void startNetwork(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(null, "Network", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tickChildren", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/players/PlayerList;tick()V", + shift = At.Shift.AFTER + )) + private void finishNetwork(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + CarpetProfiler.end_current_section(currentSection); + } + + @Inject(method = "waitUntilNextTick", at = @At("HEAD")) + private void startAsync(CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(null, "Async Tasks", CarpetProfiler.TYPE.GENERAL); + } + @Inject(method = "waitUntilNextTick", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;managedBlock(Ljava/util/function/BooleanSupplier;)V", + shift = At.Shift.BEFORE + )) + private void stopAsync(CallbackInfo ci) + { + if (CarpetProfiler.tick_health_requested != 0L) + { + CarpetProfiler.end_current_section(currentSection); + CarpetProfiler.end_tick_profiling((MinecraftServer) (Object)this); + } + } + + +} diff --git a/src/main/java/carpet/mixins/Minecraft_tickMixin.java b/src/main/java/carpet/mixins/Minecraft_tickMixin.java new file mode 100644 index 0000000..08c3f9b --- /dev/null +++ b/src/main/java/carpet/mixins/Minecraft_tickMixin.java @@ -0,0 +1,22 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.client.Minecraft; +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(Minecraft.class) +public class Minecraft_tickMixin +{ + @Inject(method = "getTickTargetMillis", at = @At("HEAD"), cancellable = true) + private void onGetTickTargetMillis(final float f, final CallbackInfoReturnable cir) + { + if (!CarpetSettings.smoothClientAnimations) { + cir.setReturnValue(f); + } + } + + +} diff --git a/src/main/java/carpet/mixins/MobCategory_spawnMixin.java b/src/main/java/carpet/mixins/MobCategory_spawnMixin.java new file mode 100644 index 0000000..dcbf87c --- /dev/null +++ b/src/main/java/carpet/mixins/MobCategory_spawnMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.fakes.SpawnGroupInterface; +import carpet.utils.SpawnReporter; +import net.minecraft.world.entity.MobCategory; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MobCategory.class) +public class MobCategory_spawnMixin implements SpawnGroupInterface +{ + @Shadow @Final private int max; + + @Inject(method = "getMaxInstancesPerChunk", at = @At("HEAD"), cancellable = true) + private void getModifiedCapacity(CallbackInfoReturnable cir) + { + cir.setReturnValue ((int) ((double)max*(Math.pow(2.0,(SpawnReporter.mobcap_exponent/4))))); + } + + @Override + public int getInitialSpawnCap() + { + return max; + } +} diff --git a/src/main/java/carpet/mixins/MobMixin.java b/src/main/java/carpet/mixins/MobMixin.java new file mode 100644 index 0000000..bd6b217 --- /dev/null +++ b/src/main/java/carpet/mixins/MobMixin.java @@ -0,0 +1,39 @@ +package carpet.mixins; + +import carpet.fakes.MobEntityInterface; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.HashMap; +import java.util.Map; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.goal.GoalSelector; + +@Mixin(Mob.class) +public abstract class MobMixin implements MobEntityInterface +{ + @Shadow @Final protected GoalSelector targetSelector; + @Shadow @Final protected GoalSelector goalSelector; + @Shadow private boolean persistenceRequired; + public final Map temporaryTasks = new HashMap<>(); + + @Override + public GoalSelector getAI(boolean target) + { + return target?targetSelector:goalSelector; + } + + @Override + public Map getTemporaryTasks() + { + return temporaryTasks; + } + + @Override + public void setPersistence(boolean what) + { + persistenceRequired = what; + } +} diff --git a/src/main/java/carpet/mixins/NaturalSpawnerMixin.java b/src/main/java/carpet/mixins/NaturalSpawnerMixin.java new file mode 100644 index 0000000..8cd2f82 --- /dev/null +++ b/src/main/java/carpet/mixins/NaturalSpawnerMixin.java @@ -0,0 +1,319 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.LevelInterface; +import carpet.utils.SpawnReporter; +import org.apache.commons.lang3.tuple.Pair; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.SpawnGroupData; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.FenceGateBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +@Mixin(NaturalSpawner.class) +public class NaturalSpawnerMixin +{ + @Shadow @Final private static int MAGIC_NUMBER; + + @Shadow @Final private static MobCategory[] SPAWNING_CATEGORIES; + + @Redirect(method = "isValidSpawnPostitionForType", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;noCollision(Lnet/minecraft/world/phys/AABB;)Z" + )) + private static boolean doesNotCollide(ServerLevel world, AABB bb) + { + //.doesNotCollide is VERY expensive. On the other side - most worlds are not made of trapdoors in + // various configurations, but solid and 'passable' blocks, like air, water grass, etc. + // checking if in the BB of the entity are only passable blocks is very cheap and covers most cases + // in case something more complex happens - we default to full block collision check + if (!CarpetSettings.lagFreeSpawning) + { + return world.noCollision(bb); + } + int minX = Mth.floor(bb.minX); + int minY = Mth.floor(bb.minY); + int minZ = Mth.floor(bb.minZ); + int maxY = Mth.ceil(bb.maxY)-1; + BlockPos.MutableBlockPos blockpos = new BlockPos.MutableBlockPos(); + if (bb.getXsize() <= 1) // small mobs + { + for (int y=minY; y <= maxY; y++) + { + blockpos.set(minX,y,minZ); + VoxelShape box = world.getBlockState(blockpos).getCollisionShape(world, blockpos); + if (box != Shapes.empty()) + { + if (box == Shapes.block()) + { + return false; + } + else + { + return world.noCollision(bb); + } + } + } + return true; + } + // this code is only applied for mobs larger than 1 block in footprint + int maxX = Mth.ceil(bb.maxX)-1; + int maxZ = Mth.ceil(bb.maxZ)-1; + for (int y = minY; y <= maxY; y++) + for (int x = minX; x <= maxX; x++) + for (int z = minZ; z <= maxZ; z++) + { + blockpos.set(x, y, z); + VoxelShape box = world.getBlockState(blockpos).getCollisionShape(world, blockpos); + if (box != Shapes.empty()) + { + if (box == Shapes.block()) + { + return false; + } + else + { + return world.noCollision(bb); + } + } + } + int min_below = minY - 1; + // we need to check blocks below for extended hitbox and in that case call + // only applies to 'large mobs', slimes, spiders, magmacubes, ghasts, etc. + for (int x = minX; x <= maxX; x++) + { + for (int z = minZ; z <= maxZ; z++) + { + blockpos.set(x, min_below, z); + BlockState state = world.getBlockState(blockpos); + Block block = state.getBlock(); + if ( + state.is(BlockTags.FENCES) || + state.is(BlockTags.WALLS) || + ((block instanceof FenceGateBlock) && !state.getValue(FenceGateBlock.OPEN)) + ) + { + if (x == minX || x == maxX || z == minZ || z == maxZ) return world.noCollision(bb); + return false; + } + } + } + return true; + } + + @Redirect(method = "getMobForSpawn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/EntityType;create(Lnet/minecraft/world/level/Level;)Lnet/minecraft/world/entity/Entity;" + )) + private static Entity create(EntityType entityType, Level world_1) + { + if (CarpetSettings.lagFreeSpawning) + { + Map, Entity> precookedMobs = ((LevelInterface)world_1).getPrecookedMobs(); + if (precookedMobs.containsKey(entityType)) + //this mob has been 's but not used yet + return precookedMobs.get(entityType); + Entity e = entityType.create(world_1); + precookedMobs.put(entityType, e); + return e; + } + return entityType.create(world_1); + } + + @Redirect(method = "spawnCategoryForPosition(Lnet/minecraft/world/entity/MobCategory;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/NaturalSpawner$SpawnPredicate;Lnet/minecraft/world/level/NaturalSpawner$AfterSpawnCallback;)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;addFreshEntityWithPassengers(Lnet/minecraft/world/entity/Entity;)V" + )) + private static void spawnEntity(ServerLevel world, Entity entity_1, + MobCategory group, ServerLevel world2, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) + { + if (CarpetSettings.lagFreeSpawning) + // we used the mob - next time we will create a new one when needed + ((LevelInterface) world).getPrecookedMobs().remove(entity_1.getType()); + + if (SpawnReporter.trackingSpawns() && SpawnReporter.local_spawns != null) + { + SpawnReporter.registerSpawn( + //world.method_27983(), // getDimensionType //dimension.getType(), // getDimensionType + (Mob) entity_1, + group, //entity_1.getType().getSpawnGroup(), + entity_1.blockPosition()); + } + if (!SpawnReporter.mockSpawns) + world.addFreshEntityWithPassengers(entity_1); + //world.spawnEntity(entity_1); + } + + @Redirect(method = "spawnCategoryForPosition(Lnet/minecraft/world/entity/MobCategory;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/NaturalSpawner$SpawnPredicate;Lnet/minecraft/world/level/NaturalSpawner$AfterSpawnCallback;)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Mob;finalizeSpawn(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/world/DifficultyInstance;Lnet/minecraft/world/entity/MobSpawnType;Lnet/minecraft/world/entity/SpawnGroupData;)Lnet/minecraft/world/entity/SpawnGroupData;" + )) + private static SpawnGroupData spawnEntity(Mob mobEntity, ServerLevelAccessor serverWorldAccess, DifficultyInstance difficulty, MobSpawnType spawnReason, SpawnGroupData entityData) + { + if (!SpawnReporter.mockSpawns) // WorldAccess + return mobEntity.finalizeSpawn(serverWorldAccess, difficulty, spawnReason, entityData); + return null; + } + + @Redirect(method = "spawnCategoryForPosition(Lnet/minecraft/world/entity/MobCategory;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/NaturalSpawner$SpawnPredicate;Lnet/minecraft/world/level/NaturalSpawner$AfterSpawnCallback;)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;distanceToSqr(DDD)D" + )) + private static double getSqDistanceTo(Player playerEntity, double double_1, double double_2, double double_3, + MobCategory entityCategory, ServerLevel serverWorld, ChunkAccess chunk, BlockPos blockPos) + { + double distanceTo = playerEntity.distanceToSqr(double_1, double_2, double_3); + if (CarpetSettings.lagFreeSpawning && distanceTo > 16384.0D && entityCategory != MobCategory.CREATURE) + return 0.0; + return distanceTo; + } + + + + //// + + @Redirect(method = "spawnForChunk", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/NaturalSpawner;spawnCategoryForChunk(Lnet/minecraft/world/entity/MobCategory;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/LevelChunk;Lnet/minecraft/world/level/NaturalSpawner$SpawnPredicate;Lnet/minecraft/world/level/NaturalSpawner$AfterSpawnCallback;)V" + )) + // inject our repeat of spawns if more spawn ticks per tick are chosen. + private static void spawnMultipleTimes(MobCategory category, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) + { + for (int i = 0; i < SpawnReporter.spawn_tries.get(category); i++) + { + NaturalSpawner.spawnCategoryForChunk(category, world, chunk, checker, runner); + } + } + + // shrug - why no inject, no idea. need to inject twice more. Will check with the names next week +/* + @Redirect(method = "spawn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/SpawnHelper$Info;isBelowCap(Lnet/minecraft/entity/SpawnGroup;)Z" + )) + // allows to change mobcaps and captures each category try per dimension before it fails due to full mobcaps. + private static boolean changeMobcaps( + SpawnHelper.Info info, SpawnGroup entityCategory, + ServerWorld serverWorld, WorldChunk chunk, SpawnHelper.Info info_outer, boolean spawnAnimals, boolean spawnMonsters, boolean shouldSpawnAnimals + ) + { + DimensionType dim = serverWorld.dimension.getType(); + int newCap = (int) ((double)entityCategory.getSpawnCap()*(Math.pow(2.0,(SpawnReporter.mobcap_exponent/4)))); + if (SpawnReporter.track_spawns > 0L) + { + int int_2 = SpawnReporter.chunkCounts.get(dim); // eligible chunks for spawning + int int_3 = newCap * int_2 / CHUNK_AREA; //current spawning limits + int mobCount = info.getCategoryToCount().getInt(entityCategory); + + if (SpawnReporter.track_spawns > 0L && !SpawnReporter.first_chunk_marker.contains(entityCategory)) + { + SpawnReporter.first_chunk_marker.add(entityCategory); + //first chunk with spawn eligibility for that category + Pair key = Pair.of(dim, entityCategory); + + + int spawnTries = SpawnReporter.spawn_tries.get(entityCategory); + + SpawnReporter.spawn_attempts.put(key, + SpawnReporter.spawn_attempts.get(key) + spawnTries); + + SpawnReporter.spawn_cap_count.put(key, + SpawnReporter.spawn_cap_count.get(key) + mobCount); + } + + if (mobCount <= int_3 || SpawnReporter.mock_spawns) + { + //place 0 to indicate there were spawn attempts for a category + //if (entityCategory != EntityCategory.CREATURE || world.getServer().getTicks() % 400 == 0) + // this will only be called once every 400 ticks anyways + SpawnReporter.local_spawns.putIfAbsent(entityCategory, 0L); + + //else + //full mobcaps - and key in local_spawns will be missing + } + } + return SpawnReporter.mock_spawns || info.getCategoryToCount().getInt(entityCategory) < newCap; + } + +*/ + //temporary mixin until naming gets fixed + + @Inject(method = "spawnForChunk", at = @At("HEAD")) + // allows to change mobcaps and captures each category try per dimension before it fails due to full mobcaps. + private static void checkSpawns(ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnState info, + boolean spawnAnimals, boolean spawnMonsters, boolean shouldSpawnAnimals, CallbackInfo ci) + { + if (SpawnReporter.trackingSpawns()) + { + MobCategory[] var6 = SPAWNING_CATEGORIES; + int var7 = var6.length; + + for(int var8 = 0; var8 < var7; ++var8) { + MobCategory entityCategory = var6[var8]; + if ((spawnAnimals || !entityCategory.isFriendly()) && (spawnMonsters || entityCategory.isFriendly()) && (shouldSpawnAnimals || !entityCategory.isPersistent()) ) + { + ResourceKey dim = world.dimension(); // getDimensionType; + int newCap = entityCategory.getMaxInstancesPerChunk(); //(int) ((double)entityCategory.getCapacity()*(Math.pow(2.0,(SpawnReporter.mobcap_exponent/4)))); + int int_2 = SpawnReporter.chunkCounts.get(dim); // eligible chunks for spawning + int int_3 = newCap * int_2 / MAGIC_NUMBER; //current spawning limits + int mobCount = info.getMobCategoryCounts().getInt(entityCategory); + + if (SpawnReporter.trackingSpawns() && !SpawnReporter.first_chunk_marker.contains(entityCategory)) + { + SpawnReporter.first_chunk_marker.add(entityCategory); + //first chunk with spawn eligibility for that category + Pair, MobCategory> key = Pair.of(dim, entityCategory); + + int spawnTries = SpawnReporter.spawn_tries.get(entityCategory); + + SpawnReporter.spawn_attempts.addTo(key, spawnTries); + + SpawnReporter.spawn_cap_count.addTo(key, mobCount); + } + + if (mobCount <= int_3 || SpawnReporter.mockSpawns) //TODO this will not float with player based mobcaps + { + //place 0 to indicate there were spawn attempts for a category + //if (entityCategory != EntityCategory.CREATURE || world.getServer().getTicks() % 400 == 0) + // this will only be called once every 400 ticks anyways + SpawnReporter.local_spawns.putIfAbsent(entityCategory, 0L); + + //else + //full mobcaps - and key in local_spawns will be missing + } + } + } + } + } + +} diff --git a/src/main/java/carpet/mixins/Objective_scarpetMixin.java b/src/main/java/carpet/mixins/Objective_scarpetMixin.java new file mode 100644 index 0000000..1c23961 --- /dev/null +++ b/src/main/java/carpet/mixins/Objective_scarpetMixin.java @@ -0,0 +1,13 @@ +package carpet.mixins; + +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Objective.class) +public interface Objective_scarpetMixin { + @Mutable @Accessor("criteria") + void setCriterion(ObjectiveCriteria criterion); +} diff --git a/src/main/java/carpet/mixins/PathNavigation_pathfindingMixin.java b/src/main/java/carpet/mixins/PathNavigation_pathfindingMixin.java new file mode 100644 index 0000000..3b4abc8 --- /dev/null +++ b/src/main/java/carpet/mixins/PathNavigation_pathfindingMixin.java @@ -0,0 +1,59 @@ +package carpet.mixins; + +import carpet.logging.LoggerRegistry; +import carpet.logging.logHelpers.PathfindingVisualizer; +import org.jetbrains.annotations.Nullable; +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; + +import java.util.Set; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.phys.Vec3; + +@Mixin(PathNavigation.class) +public abstract class PathNavigation_pathfindingMixin +{ + + @Shadow @Final protected Mob mob; + + + @Shadow protected @Nullable abstract Path createPath(Set set, int i, boolean bl, int j); + + @Redirect(method = "createPath(Lnet/minecraft/core/BlockPos;I)Lnet/minecraft/world/level/pathfinder/Path;", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/ai/navigation/PathNavigation;createPath(Ljava/util/Set;IZI)Lnet/minecraft/world/level/pathfinder/Path;" + )) + private Path pathToBlock(PathNavigation entityNavigation, Set set_1, int int_1, boolean boolean_1, int int_2) + { + if (!LoggerRegistry.__pathfinding) + return createPath(set_1, int_1, boolean_1, int_2); + long start = System.nanoTime(); + Path path = createPath(set_1, int_1, boolean_1, int_2); + long finish = System.nanoTime(); + float duration = (1.0F*((finish - start)/1000))/1000; + set_1.forEach(b -> PathfindingVisualizer.slowPath(mob, Vec3.atBottomCenterOf(b), duration, path != null)); // ground centered position + return path; + } + + @Redirect(method = "createPath(Lnet/minecraft/world/entity/Entity;I)Lnet/minecraft/world/level/pathfinder/Path;", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/ai/navigation/PathNavigation;createPath(Ljava/util/Set;IZI)Lnet/minecraft/world/level/pathfinder/Path;" + )) + private Path pathToEntity(PathNavigation entityNavigation, Set set_1, int int_1, boolean boolean_1, int int_2) + { + if (!LoggerRegistry.__pathfinding) + return createPath(set_1, int_1, boolean_1, int_2); + long start = System.nanoTime(); + Path path = createPath(set_1, int_1, boolean_1, int_2); + long finish = System.nanoTime(); + float duration = (1.0F*((finish - start)/1000))/1000; + set_1.forEach(b -> PathfindingVisualizer.slowPath(mob, Vec3.atBottomCenterOf(b), duration, path != null)); + return path; + } +} diff --git a/src/main/java/carpet/mixins/PerfCommand_permissionMixin.java b/src/main/java/carpet/mixins/PerfCommand_permissionMixin.java new file mode 100644 index 0000000..a07fdd4 --- /dev/null +++ b/src/main/java/carpet/mixins/PerfCommand_permissionMixin.java @@ -0,0 +1,20 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.commands.PerfCommand; +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(PerfCommand.class) +public class PerfCommand_permissionMixin +{ + @Inject(method = "method_37340", at = @At("HEAD"), cancellable = true, remap = false) + private static void canRun(CommandSourceStack source, CallbackInfoReturnable cir) + { + cir.setReturnValue(source.hasPermission(CarpetSettings.perfPermissionLevel)); + } + +} diff --git a/src/main/java/carpet/mixins/PersistentEntitySectionManager_scarpetMixin.java b/src/main/java/carpet/mixins/PersistentEntitySectionManager_scarpetMixin.java new file mode 100644 index 0000000..dabff91 --- /dev/null +++ b/src/main/java/carpet/mixins/PersistentEntitySectionManager_scarpetMixin.java @@ -0,0 +1,51 @@ +package carpet.mixins; + +import carpet.script.CarpetEventServer; +import carpet.script.CarpetScriptServer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +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(PersistentEntitySectionManager.class) +public class PersistentEntitySectionManager_scarpetMixin +{ + @Inject(method = "addEntity(Lnet/minecraft/world/level/entity/EntityAccess;Z)Z", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/Visibility;isTicking()Z" + )) + private void handleAddedEntity(EntityAccess entityLike, boolean existing, CallbackInfoReturnable cir) + { + Entity entity = (Entity)entityLike; + CarpetEventServer.Event event = CarpetEventServer.Event.ENTITY_HANDLER.get(entity.getType()); + if (event != null) + { + if (event.isNeeded()) + { + event.onEntityAction(entity, !existing); + } + } + else + { + CarpetScriptServer.LOG.error("Failed to handle entity type " + entity.getType().getDescriptionId()); + } + + event = CarpetEventServer.Event.ENTITY_LOAD.get(entity.getType()); + if (event != null) + { + if (event.isNeeded()) + { + event.onEntityAction(entity, true); + } + } + else + { + CarpetScriptServer.LOG.error("Failed to handle entity type " + entity.getType().getDescriptionId()); + } + + } + +} diff --git a/src/main/java/carpet/mixins/PickaxeItem_missingToolsMixin.java b/src/main/java/carpet/mixins/PickaxeItem_missingToolsMixin.java new file mode 100644 index 0000000..a8e9a3f --- /dev/null +++ b/src/main/java/carpet/mixins/PickaxeItem_missingToolsMixin.java @@ -0,0 +1,34 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.component.DataComponents; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.DiggerItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.PickaxeItem; +import net.minecraft.world.item.Tier; +import net.minecraft.world.item.component.Tool; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(PickaxeItem.class) +public class PickaxeItem_missingToolsMixin extends DiggerItem +{ + + protected PickaxeItem_missingToolsMixin(Tier material, TagKey tag, Properties settings) { + super(material, tag, settings); + } + + @Override + public float getDestroySpeed(ItemStack stack, BlockState state) { + if (CarpetSettings.missingTools && state.getSoundType() == SoundType.GLASS) + { + final Tool tool = stack.get(DataComponents.TOOL); + return tool != null ? tool.getMiningSpeed(Blocks.STONE.defaultBlockState()) : super.getDestroySpeed(stack, state); + } + return super.getDestroySpeed(stack, state); + } +} diff --git a/src/main/java/carpet/mixins/PieceGeneratorSupplier_plopMixin.java b/src/main/java/carpet/mixins/PieceGeneratorSupplier_plopMixin.java new file mode 100644 index 0000000..79c4e1c --- /dev/null +++ b/src/main/java/carpet/mixins/PieceGeneratorSupplier_plopMixin.java @@ -0,0 +1,22 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.function.Predicate; +import net.minecraft.world.level.levelgen.structure.pieces.PieceGeneratorSupplier; + +@Mixin(PieceGeneratorSupplier.class) +public interface PieceGeneratorSupplier_plopMixin +{ + @Redirect(method = "method_39845", at = @At( + value = "INVOKE", + target = "java/util/function/Predicate.test(Ljava/lang/Object;)Z" + ), remap = false) + private static boolean checkMate(Predicate predicate, Object o) + { + return CarpetSettings.skipGenerationChecks.get() || predicate.test(o); + } +} diff --git a/src/main/java/carpet/mixins/PiglinBrute_getPlacementTypeMixin.java b/src/main/java/carpet/mixins/PiglinBrute_getPlacementTypeMixin.java new file mode 100644 index 0000000..3b5b908 --- /dev/null +++ b/src/main/java/carpet/mixins/PiglinBrute_getPlacementTypeMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.SpawnPlacementType; +import net.minecraft.world.entity.SpawnPlacementTypes; +import net.minecraft.world.entity.SpawnPlacements; +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(SpawnPlacements.class) +public class PiglinBrute_getPlacementTypeMixin { + @Inject(method = "getPlacementType", at = @At("HEAD"), cancellable = true) + private static void getPlacementType(final EntityType entityType, final CallbackInfoReturnable cir) { + if (CarpetSettings.piglinsSpawningInBastions && entityType == EntityType.PIGLIN_BRUTE) { + cir.setReturnValue(SpawnPlacementTypes.ON_GROUND); + } + } +} diff --git a/src/main/java/carpet/mixins/PistonBaseBlock_movableBEMixin.java b/src/main/java/carpet/mixins/PistonBaseBlock_movableBEMixin.java new file mode 100644 index 0000000..ef1a2d3 --- /dev/null +++ b/src/main/java/carpet/mixins/PistonBaseBlock_movableBEMixin.java @@ -0,0 +1,139 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.PistonBlockEntityInterface; +import com.google.common.collect.Lists; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.CommandBlock; +import net.minecraft.world.level.block.DirectionalBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.piston.MovingPistonBlock; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import net.minecraft.world.level.block.piston.PistonStructureResolver; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.PushReaction; +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.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.List; +import java.util.Map; + +@Mixin(PistonBaseBlock.class) +public abstract class PistonBaseBlock_movableBEMixin extends DirectionalBlock +{ + protected PistonBaseBlock_movableBEMixin(Properties block$Settings_1) + { + super(block$Settings_1); + } + + private ThreadLocal> list1_BlockEntities = new ThreadLocal<>(); //Unneccessary ThreadLocal if client and server use different PistonBlock instances + + @Inject(method = "isPushable", cancellable = true, at = @At(value = "RETURN", ordinal = 3, shift = At.Shift.BEFORE)) + private static void movableCMD(BlockState blockState_1, Level world_1, BlockPos blockPos_1, + Direction direction_1, boolean boolean_1, Direction direction_2, CallbackInfoReturnable cir) + { + Block block_1 = blockState_1.getBlock(); + //Make CommandBlocks movable, either use instanceof CommandBlock or the 3 cmd block objects, + if (CarpetSettings.movableBlockEntities && block_1 instanceof CommandBlock) + { + cir.setReturnValue(true); + } + } + + private static boolean isPushableBlockEntity(Block block) + { + //Making PISTON_EXTENSION (BlockPistonMoving) pushable would not work as its createNewTileEntity()-method returns null + return block != Blocks.ENDER_CHEST && block != Blocks.ENCHANTING_TABLE && + block != Blocks.END_GATEWAY && block != Blocks.END_PORTAL && block != Blocks.MOVING_PISTON && + block != Blocks.SPAWNER; + } + + @Redirect(method = "isPushable", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;hasBlockEntity()Z")) + private static boolean ifHasBlockEntity(BlockState blockState) + { + if (!blockState.hasBlockEntity()) + { + return false; + } + else + { + return !(CarpetSettings.movableBlockEntities && isPushableBlockEntity(blockState.getBlock())); + } + } + + @Redirect(method = "isPushable", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;getPistonPushReaction()Lnet/minecraft/world/level/material/PushReaction;" + )) + private static PushReaction moveGrindstones(BlockState blockState) + { + if (CarpetSettings.movableBlockEntities && blockState.getBlock() == Blocks.GRINDSTONE) return PushReaction.NORMAL; + return blockState.getPistonPushReaction(); + } + + @Inject(method = "moveBlocks", at = @At(value = "INVOKE", shift = At.Shift.BEFORE, + target = "Ljava/util/List;size()I", ordinal = 3),locals = LocalCapture.CAPTURE_FAILHARD) + private void onMove(Level world_1, BlockPos blockPos_1, Direction direction_1, boolean boolean_1, + CallbackInfoReturnable cir, BlockPos blockPos_2, PistonStructureResolver pistonHandler_1, Map map_1, + List list_1, List list_2, List list_3, BlockState[] blockStates_1, + Direction direction_2, int int_2) + { + //Get the blockEntities and remove them from the world before any magic starts to happen + if (CarpetSettings.movableBlockEntities) + { + list1_BlockEntities.set(Lists.newArrayList()); + for (int i = 0; i < list_1.size(); ++i) + { + BlockPos blockpos = list_1.get(i); + BlockEntity blockEntity = (list_2.get(i).hasBlockEntity()) ? world_1.getBlockEntity(blockpos) : null; + list1_BlockEntities.get().add(blockEntity); + if (blockEntity != null) + { + //hopefully this call won't have any side effects in the future, such as dropping all the BlockEntity's items + //we want to place this same(!) BlockEntity object into the world later when the movement stops again + world_1.removeBlockEntity(blockpos); + blockEntity.setChanged(); + } + } + } + } + + @Inject(method = "moveBlocks", at = @At(value = "INVOKE", shift = At.Shift.BEFORE, + target = "Lnet/minecraft/world/level/Level;setBlockEntity(Lnet/minecraft/world/level/block/entity/BlockEntity;)V", ordinal = 0), + locals = LocalCapture.CAPTURE_FAILHARD) + private void setBlockEntityWithCarried(Level world_1, BlockPos blockPos_1, Direction direction_1, boolean boolean_1, + CallbackInfoReturnable cir, BlockPos blockPos_2, PistonStructureResolver pistonHandler_1, Map map_1, List list_1, + List list_2, List list_3, BlockState[] blockStates_1, Direction direction_2, int int_2, + int int_3, BlockPos blockPos_4, BlockState blockState9, BlockState blockState4) + { + BlockEntity blockEntityPiston = MovingPistonBlock.newMovingBlockEntity(blockPos_4, blockState4, list_2.get(int_3), + direction_1, boolean_1, false); + if (CarpetSettings.movableBlockEntities) + ((PistonBlockEntityInterface) blockEntityPiston).setCarriedBlockEntity(list1_BlockEntities.get().get(int_3)); + world_1.setBlockEntity(blockEntityPiston); + //world_1.setBlockEntity(blockPos_4, blockEntityPiston); + } + + @Redirect(method = "moveBlocks", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;setBlockEntity(Lnet/minecraft/world/level/block/entity/BlockEntity;)V", + ordinal = 0)) + private void dontDoAnything(Level world, BlockEntity blockEntity) + { + } + + @Redirect(method = "moveBlocks", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/piston/MovingPistonBlock;newMovingBlockEntity(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/Direction;ZZ)Lnet/minecraft/world/level/block/entity/BlockEntity;", + ordinal = 0)) + private BlockEntity returnNull(BlockPos blockPos, BlockState blockState, BlockState blockState2, Direction direction, boolean bl, boolean bl2) + { + return null; + } +} diff --git a/src/main/java/carpet/mixins/PistonBaseBlock_qcMixin.java b/src/main/java/carpet/mixins/PistonBaseBlock_qcMixin.java new file mode 100644 index 0000000..f0b3d1a --- /dev/null +++ b/src/main/java/carpet/mixins/PistonBaseBlock_qcMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import net.minecraft.world.level.SignalGetter; +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 carpet.helpers.QuasiConnectivity; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.piston.PistonBaseBlock; + +@Mixin(PistonBaseBlock.class) +public class PistonBaseBlock_qcMixin { + + @Inject( + method = "getNeighborSignal", + cancellable = true, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/core/BlockPos;above()Lnet/minecraft/core/BlockPos;" + ) + ) + private void carpet_checkQuasiSignal(SignalGetter level, BlockPos pos, Direction facing, CallbackInfoReturnable cir) { + cir.setReturnValue(QuasiConnectivity.hasQuasiSignal(level, pos)); + } +} diff --git a/src/main/java/carpet/mixins/PistonBaseBlock_rotatorBlockMixin.java b/src/main/java/carpet/mixins/PistonBaseBlock_rotatorBlockMixin.java new file mode 100644 index 0000000..225a7eb --- /dev/null +++ b/src/main/java/carpet/mixins/PistonBaseBlock_rotatorBlockMixin.java @@ -0,0 +1,22 @@ +package carpet.mixins; + +import carpet.fakes.PistonBlockInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.SignalGetter; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(PistonBaseBlock.class) +public abstract class PistonBaseBlock_rotatorBlockMixin implements PistonBlockInterface +{ + @Shadow protected abstract boolean getNeighborSignal(SignalGetter world_1, BlockPos blockPos_1, Direction direction_1); + + @Override + public boolean publicShouldExtend(Level world_1, BlockPos blockPos_1, Direction direction_1) + { + return getNeighborSignal(world_1, blockPos_1,direction_1); + } +} diff --git a/src/main/java/carpet/mixins/PistonHeadRenderer_movableBEMixin.java b/src/main/java/carpet/mixins/PistonHeadRenderer_movableBEMixin.java new file mode 100644 index 0000000..87df4e3 --- /dev/null +++ b/src/main/java/carpet/mixins/PistonHeadRenderer_movableBEMixin.java @@ -0,0 +1,59 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.PistonBlockEntityInterface; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderDispatcher; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.PistonHeadRenderer; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; +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; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(PistonHeadRenderer.class) +public abstract class PistonHeadRenderer_movableBEMixin implements BlockEntityRenderer +{ + BlockEntityRenderDispatcher dispatcher; + @Inject(method = "", at = @At("TAIL")) + private void onInitCM(BlockEntityRendererProvider.Context arguments, CallbackInfo ci) + { + dispatcher = arguments.getBlockEntityRenderDispatcher(); + } + + @Inject(method = "render", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/renderer/blockentity/PistonHeadRenderer;renderBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;Lnet/minecraft/world/level/Level;ZI)V", ordinal = 3)) + private void updateRenderBool(PistonMovingBlockEntity pistonBlockEntity_1, float float_1, PoseStack matrixStack_1, MultiBufferSource layeredVertexConsumerStorage_1, int int_1, int int_2, CallbackInfo ci) + //private void updateRenderBool(PistonBlockEntity pistonBlockEntity_1, double double_1, double double_2, double double_3, float float_1, class_4587 class_4587_1, class_4597 class_4597_1, int int_1, CallbackInfo ci) + { + if (!((PistonBlockEntityInterface) pistonBlockEntity_1).isRenderModeSet()) + ((PistonBlockEntityInterface) pistonBlockEntity_1).setRenderCarriedBlockEntity(CarpetSettings.movableBlockEntities && ((PistonBlockEntityInterface) pistonBlockEntity_1).getCarriedBlockEntity() != null); + } + + + @Inject(method = "render", at = @At("RETURN"), locals = LocalCapture.NO_CAPTURE) + private void endMethod3576(PistonMovingBlockEntity pistonBlockEntity_1, float partialTicks, PoseStack matrixStack_1, MultiBufferSource layeredVertexConsumerStorage_1, int int_1, int init_2, CallbackInfo ci) + { + if (((PistonBlockEntityInterface) pistonBlockEntity_1).getRenderCarriedBlockEntity()) + { + BlockEntity carriedBlockEntity = ((PistonBlockEntityInterface) pistonBlockEntity_1).getCarriedBlockEntity(); + if (carriedBlockEntity != null) + { + // maybe ??? carriedBlockEntity.setPos(pistonBlockEntity_1.getPos()); + //((BlockEntityRenderDispatcherInterface) BlockEntityRenderDispatcher.INSTANCE).renderBlockEntityOffset(carriedBlockEntity, float_1, int_1, BlockRenderLayer.field_20799, bufferBuilder_1, pistonBlockEntity_1.getRenderOffsetX(float_1), pistonBlockEntity_1.getRenderOffsetY(float_1), pistonBlockEntity_1.getRenderOffsetZ(float_1)); + matrixStack_1.translate( + pistonBlockEntity_1.getXOff(partialTicks), + pistonBlockEntity_1.getYOff(partialTicks), + pistonBlockEntity_1.getZOff(partialTicks) + ); + dispatcher.render(carriedBlockEntity, partialTicks, matrixStack_1, layeredVertexConsumerStorage_1); + + } + } + } +} diff --git a/src/main/java/carpet/mixins/PistonMovingBlockEntity_movableBEMixin.java b/src/main/java/carpet/mixins/PistonMovingBlockEntity_movableBEMixin.java new file mode 100644 index 0000000..b7d01d7 --- /dev/null +++ b/src/main/java/carpet/mixins/PistonMovingBlockEntity_movableBEMixin.java @@ -0,0 +1,158 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.BlockEntityInterface; +import carpet.fakes.PistonBlockEntityInterface; +import carpet.fakes.LevelInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +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; + +@Mixin(PistonMovingBlockEntity.class) +public abstract class PistonMovingBlockEntity_movableBEMixin extends BlockEntity implements PistonBlockEntityInterface +{ + @Shadow + private boolean isSourcePiston; + @Shadow + private BlockState movedState; + + private BlockEntity carriedBlockEntity; + private boolean renderCarriedBlockEntity = false; + private boolean renderSet = false; + + public PistonMovingBlockEntity_movableBEMixin(BlockEntityType blockEntityType, BlockPos blockPos, BlockState blockState) { + super(blockEntityType, blockPos, blockState); + } + + + /** + * @author 2No2Name + */ + @Override + public BlockEntity getCarriedBlockEntity() + { + return carriedBlockEntity; + } + + @Override + public void setLevel(Level world) { + super.setLevel(world); + if (carriedBlockEntity != null) carriedBlockEntity.setLevel(world); + } + + @Override + public void setCarriedBlockEntity(BlockEntity blockEntity) + { + this.carriedBlockEntity = blockEntity; + if (this.carriedBlockEntity != null) + { + ((BlockEntityInterface)carriedBlockEntity).setCMPos(worldPosition); + // this might be little dangerous since pos is final for a hashing reason? + if (level != null) carriedBlockEntity.setLevel(level); + } + // this.carriedBlockEntity.setPos(this.pos); + } + + @Override + public boolean isRenderModeSet() + { + return renderSet; + } + + @Override + public boolean getRenderCarriedBlockEntity() + { + return renderCarriedBlockEntity; + } + + @Override + public void setRenderCarriedBlockEntity(boolean b) + { + renderCarriedBlockEntity = b; + renderSet = true; + } + + /** + * @author 2No2Name + */ + @Redirect(method = "tick", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z")) + private static boolean movableTEsetBlockState0( + Level world, BlockPos blockPos_1, BlockState blockAState_2, int int_1, + Level world2, BlockPos blockPos, BlockState blockState, PistonMovingBlockEntity pistonBlockEntity) + { + if (!CarpetSettings.movableBlockEntities) + return world.setBlock(blockPos_1, blockAState_2, int_1); + else + return ((LevelInterface) (world)).setBlockStateWithBlockEntity(blockPos_1, blockAState_2, ((PistonBlockEntityInterface)pistonBlockEntity).getCarriedBlockEntity(), int_1); + } + + @Redirect(method = "finalTick", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z")) + private boolean movableTEsetBlockState1(Level world, BlockPos blockPos_1, BlockState blockState_2, int int_1) + { + if (!CarpetSettings.movableBlockEntities) + return world.setBlock(blockPos_1, blockState_2, int_1); + else + { + boolean ret = ((LevelInterface) (world)).setBlockStateWithBlockEntity(blockPos_1, blockState_2, this.carriedBlockEntity, int_1); + this.carriedBlockEntity = null; //this will cancel the finishHandleBroken + return ret; + } + } + + @Inject(method = "finalTick", at = @At(value = "RETURN")) + private void finishHandleBroken(CallbackInfo cir) + { + //Handle TNT Explosions or other ways the moving Block is broken + //Also /setblock will cause this to be called, and drop e.g. a moving chest's contents. + // This is MC-40380 (BlockEntities that aren't Inventories drop stuff when setblock is called ) + if (CarpetSettings.movableBlockEntities && this.carriedBlockEntity != null && !this.level.isClientSide && this.level.getBlockState(this.worldPosition).getBlock() == Blocks.AIR) + { + BlockState blockState_2; + if (this.isSourcePiston) + blockState_2 = Blocks.AIR.defaultBlockState(); + else + blockState_2 = Block.updateFromNeighbourShapes(this.movedState, this.level, this.worldPosition); + ((LevelInterface) (this.level)).setBlockStateWithBlockEntity(this.worldPosition, blockState_2, this.carriedBlockEntity, 3); + this.level.destroyBlock(this.worldPosition, false, null); + } + } + + @Inject(method = "loadAdditional", at = @At(value = "TAIL")) + private void onFromTag(CompoundTag NbtCompound_1, HolderLookup.Provider registries, CallbackInfo ci) + { + if (CarpetSettings.movableBlockEntities && NbtCompound_1.contains("carriedTileEntityCM", 10)) + { + if (this.movedState.getBlock() instanceof EntityBlock) + this.carriedBlockEntity = ((EntityBlock) (this.movedState.getBlock())).newBlockEntity(worldPosition, movedState);// this.world); + if (carriedBlockEntity != null) //Can actually be null, as BlockPistonMoving.createNewTileEntity(...) returns null + this.carriedBlockEntity.loadWithComponents(NbtCompound_1.getCompound("carriedTileEntityCM"), registries); + setCarriedBlockEntity(carriedBlockEntity); + } + } + + @Inject(method = "saveAdditional", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) + private void onToTag(CompoundTag NbtCompound_1, HolderLookup.Provider registries, CallbackInfo ci) + { + if (CarpetSettings.movableBlockEntities && this.carriedBlockEntity != null) + { + //Leave name "carriedTileEntityCM" instead of "carriedBlockEntityCM" for upgrade compatibility with 1.13.2 movable TE + NbtCompound_1.put("carriedTileEntityCM", this.carriedBlockEntity.saveWithoutMetadata(registries)); + } + } +} diff --git a/src/main/java/carpet/mixins/PistonMovingBlockEntity_playerHandlingMixin.java b/src/main/java/carpet/mixins/PistonMovingBlockEntity_playerHandlingMixin.java new file mode 100644 index 0000000..cd5106a --- /dev/null +++ b/src/main/java/carpet/mixins/PistonMovingBlockEntity_playerHandlingMixin.java @@ -0,0 +1,61 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.patches.EntityPlayerMPFake; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.piston.PistonMovingBlockEntity; +import net.minecraft.world.level.material.PushReaction; +import net.minecraft.world.phys.Vec3; +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(PistonMovingBlockEntity.class) +public abstract class PistonMovingBlockEntity_playerHandlingMixin +{ + @Inject(method = "moveEntityByPiston", at = @At("HEAD"), cancellable = true) + private static void dontPushSpectators(Direction direction, Entity entity, double d, Direction direction2, CallbackInfo ci) + { + if (CarpetSettings.creativeNoClip && entity instanceof Player && (((Player) entity).isCreative()) && ((Player) entity).getAbilities().flying) ci.cancel(); + } + + @Redirect(method = "moveCollidedEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;setDeltaMovement(DDD)V")) + private static void ignoreAccel(Entity entity, double x, double y, double z) + { + if (CarpetSettings.creativeNoClip && entity instanceof Player && (((Player) entity).isCreative()) && ((Player) entity).getAbilities().flying) return; + entity.setDeltaMovement(x,y,z); + } + + @Redirect(method = "moveCollidedEntities", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;getPistonPushReaction()Lnet/minecraft/world/level/material/PushReaction;" + )) + private static PushReaction moveFakePlayers(Entity entity, + Level world, BlockPos blockPos, float ff, PistonMovingBlockEntity pistonBlockEntity) + { + if (entity instanceof EntityPlayerMPFake && pistonBlockEntity.getMovedState().is(Blocks.SLIME_BLOCK)) + { + Vec3 vec3d = entity.getDeltaMovement(); + double x = vec3d.x; + double y = vec3d.y; + double z = vec3d.z; + Direction direction = pistonBlockEntity.getMovementDirection(); + switch (direction.getAxis()) { + case X -> x = direction.getStepX(); + case Y -> y = direction.getStepY(); + case Z -> z = direction.getStepZ(); + } + + entity.setDeltaMovement(x, y, z); + } + return entity.getPistonPushReaction(); + } + +} diff --git a/src/main/java/carpet/mixins/PistonStructureResolver_customStickyMixin.java b/src/main/java/carpet/mixins/PistonStructureResolver_customStickyMixin.java new file mode 100644 index 0000000..88ab4fd --- /dev/null +++ b/src/main/java/carpet/mixins/PistonStructureResolver_customStickyMixin.java @@ -0,0 +1,109 @@ +package carpet.mixins; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +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.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import carpet.fakes.BlockPistonBehaviourInterface; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.piston.PistonStructureResolver; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(PistonStructureResolver.class) +public class PistonStructureResolver_customStickyMixin { + + @Shadow @Final private Level level; + @Shadow @Final private Direction pushDirection; + + @Shadow private static boolean canStickToEachOther(BlockState blockState, BlockState blockState2) { + throw new AssertionError(); + } + + @Inject( + method = "isSticky", + cancellable = true, + at = @At( + value = "HEAD" + ) + ) + private static void isSticky(BlockState state, CallbackInfoReturnable cir) { + if (state.getBlock() instanceof BlockPistonBehaviourInterface behaviourInterface){ + cir.setReturnValue(behaviourInterface.isSticky(state)); + } + } + + // fields that are needed because @Redirects cannot capture locals + @Unique private BlockPos pos_addBlockLine; + @Unique private BlockPos behindPos_addBlockLine; + + @Inject( + method = "addBlockLine", + locals = LocalCapture.CAPTURE_FAILHARD, + at = @At( + value = "INVOKE", + ordinal = 1, + target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;" + ) + ) + private void captureBlockLinePositions(BlockPos pos, Direction fromDir, CallbackInfoReturnable cir, BlockState state, int dst, BlockPos behindPos) { + pos_addBlockLine = behindPos.relative(pushDirection); + behindPos_addBlockLine = behindPos; + } + + @Redirect( + method = "addBlockLine", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/piston/PistonStructureResolver;canStickToEachOther(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/state/BlockState;)Z" + ) + ) + private boolean onAddBlockLineCanStickToEachOther(BlockState state, BlockState behindState) { + if (state.getBlock() instanceof BlockPistonBehaviourInterface behaviourInterface) { + return behaviourInterface.isStickyToNeighbor(level, pos_addBlockLine, state, behindPos_addBlockLine, behindState, pushDirection.getOpposite(), pushDirection); + } + + return canStickToEachOther(state, behindState); + } + + // fields that are needed because @Redirects cannot capture locals + @Unique private Direction dir_addBranchingBlocks; + @Unique private BlockPos neighborPos_addBranchingBlocks; + + @Inject( + method = "addBranchingBlocks", + locals = LocalCapture.CAPTURE_FAILHARD, + at = @At( + value = "INVOKE", + ordinal = 1, + target = "Lnet/minecraft/world/level/Level;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;" + ) + ) + private void captureNeighborPositions(BlockPos pos, CallbackInfoReturnable cir, BlockState state, Direction[] dirs, int i, int j, Direction dir, BlockPos neighborPos) { + dir_addBranchingBlocks = dir; + neighborPos_addBranchingBlocks = neighborPos; + } + + @Redirect( + method = "addBranchingBlocks", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/piston/PistonStructureResolver;canStickToEachOther(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/block/state/BlockState;)Z" + ) + ) + private boolean onAddBranchingBlocksCanStickToEachOther(BlockState neighborState, BlockState state, BlockPos pos) { + if (state.getBlock() instanceof BlockPistonBehaviourInterface behaviourInterface) { + return behaviourInterface.isStickyToNeighbor(level, pos, state, neighborPos_addBranchingBlocks, neighborState, dir_addBranchingBlocks, pushDirection); + } + + return canStickToEachOther(neighborState, state); + } +} diff --git a/src/main/java/carpet/mixins/PistonStructureResolver_pushLimitMixin.java b/src/main/java/carpet/mixins/PistonStructureResolver_pushLimitMixin.java new file mode 100644 index 0000000..d2a3cc8 --- /dev/null +++ b/src/main/java/carpet/mixins/PistonStructureResolver_pushLimitMixin.java @@ -0,0 +1,17 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.level.block.piston.PistonStructureResolver; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(value = PistonStructureResolver.class, priority = 420) // piston push limit is important for carpet +public class PistonStructureResolver_pushLimitMixin +{ + @ModifyConstant(method = "addBlockLine", constant = @Constant(intValue = PistonStructureResolver.MAX_PUSH_DEPTH), expect = 3) + private int pushLimit(int original) + { + return CarpetSettings.pushLimit; + } +} diff --git a/src/main/java/carpet/mixins/PlayerList_coreMixin.java b/src/main/java/carpet/mixins/PlayerList_coreMixin.java new file mode 100644 index 0000000..fec5c66 --- /dev/null +++ b/src/main/java/carpet/mixins/PlayerList_coreMixin.java @@ -0,0 +1,30 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import carpet.network.ServerNetworkHandler; +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +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(PlayerList.class) +public class PlayerList_coreMixin +{ + + @Inject(method = "placeNewPlayer", at = @At("RETURN")) + private void onPlayerConnected(Connection connection, ServerPlayer player, CommonListenerCookie i, CallbackInfo ci) + { + CarpetServer.onPlayerLoggedIn(player); + } + + @Inject(method = "sendLevelInfo", at = @At("RETURN")) + private void onLevelChanged(final ServerPlayer serverPlayer, final ServerLevel serverLevel, final CallbackInfo ci) + { + ServerNetworkHandler.sendPlayerLevelData(serverPlayer, serverLevel); + } +} diff --git a/src/main/java/carpet/mixins/PlayerList_fakePlayersMixin.java b/src/main/java/carpet/mixins/PlayerList_fakePlayersMixin.java new file mode 100644 index 0000000..3cd54ed --- /dev/null +++ b/src/main/java/carpet/mixins/PlayerList_fakePlayersMixin.java @@ -0,0 +1,60 @@ +package carpet.mixins; + +import com.mojang.authlib.GameProfile; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import carpet.patches.NetHandlerPlayServerFake; +import carpet.patches.EntityPlayerMPFake; + +@Mixin(PlayerList.class) +public abstract class PlayerList_fakePlayersMixin +{ + @Shadow + @Final + private MinecraftServer server; + + @Inject(method = "load", at = @At(value = "RETURN", shift = At.Shift.BEFORE)) + private void fixStartingPos(ServerPlayer serverPlayerEntity_1, CallbackInfoReturnable cir) + { + if (serverPlayerEntity_1 instanceof EntityPlayerMPFake) + { + ((EntityPlayerMPFake) serverPlayerEntity_1).fixStartingPosition.run(); + } + } + + @Redirect(method = "placeNewPlayer", at = @At(value = "NEW", target = "(Lnet/minecraft/server/MinecraftServer;Lnet/minecraft/network/Connection;Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/server/network/CommonListenerCookie;)Lnet/minecraft/server/network/ServerGamePacketListenerImpl;")) + private ServerGamePacketListenerImpl replaceNetworkHandler(MinecraftServer server, Connection clientConnection, ServerPlayer playerIn, CommonListenerCookie cookie) + { + if (playerIn instanceof EntityPlayerMPFake fake) + { + return new NetHandlerPlayServerFake(this.server, clientConnection, fake, cookie); + } + else + { + return new ServerGamePacketListenerImpl(this.server, clientConnection, playerIn, cookie); + } + } + + @Redirect(method = "respawn", at = @At(value = "NEW", target = "(Lnet/minecraft/server/MinecraftServer;Lnet/minecraft/server/level/ServerLevel;Lcom/mojang/authlib/GameProfile;Lnet/minecraft/server/level/ClientInformation;)Lnet/minecraft/server/level/ServerPlayer;")) + public ServerPlayer makePlayerForRespawn(MinecraftServer minecraftServer, ServerLevel serverLevel, GameProfile gameProfile, ClientInformation cli, ServerPlayer serverPlayer, boolean i) + { + if (serverPlayer instanceof EntityPlayerMPFake) { + return EntityPlayerMPFake.respawnFake(minecraftServer, serverLevel, gameProfile, cli); + } + return new ServerPlayer(minecraftServer, serverLevel, gameProfile, cli); + } +} diff --git a/src/main/java/carpet/mixins/PlayerList_scarpetEventsMixin.java b/src/main/java/carpet/mixins/PlayerList_scarpetEventsMixin.java new file mode 100644 index 0000000..8178a76 --- /dev/null +++ b/src/main/java/carpet/mixins/PlayerList_scarpetEventsMixin.java @@ -0,0 +1,58 @@ +package carpet.mixins; + +import carpet.fakes.ServerPlayerInterface; +import carpet.script.external.Vanilla; +import net.minecraft.network.chat.ChatType; +import net.minecraft.network.chat.PlayerChatMessage; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import static carpet.script.CarpetEventServer.Event.PLAYER_MESSAGE; +import static carpet.script.CarpetEventServer.Event.PLAYER_RESPAWNS; + +@Mixin(PlayerList.class) +public class PlayerList_scarpetEventsMixin +{ + @Shadow @Final private MinecraftServer server; + + @Inject(method = "respawn", at = @At("HEAD")) + private void onResp(ServerPlayer serverPlayer, boolean olive, Entity.RemovalReason removalReason, CallbackInfoReturnable cir) + { + PLAYER_RESPAWNS.onPlayerEvent(serverPlayer); + } + + @Inject(method = "broadcastChatMessage(Lnet/minecraft/network/chat/PlayerChatMessage;Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/network/chat/ChatType$Bound;)V", + at = @At("HEAD"), + cancellable = true) + private void cancellableChatMessageEvent(PlayerChatMessage message, ServerPlayer player, ChatType.Bound params, CallbackInfo ci) { + // having this earlier breaks signatures + if (PLAYER_MESSAGE.isNeeded()) + { + if (PLAYER_MESSAGE.onPlayerMessage(player, message.signedContent())) ci.cancel(); + } + } + + @Inject(method = "respawn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;initInventoryMenu()V" + )) + private void invalidatePreviousInstance(ServerPlayer player, boolean alive, Entity.RemovalReason removalReason, CallbackInfoReturnable cir) + { + ((ServerPlayerInterface)player).invalidateEntityObjectReference(); + } + + @Inject(method = "reloadResources", at = @At("HEAD")) + private void reloadCommands(CallbackInfo ci) + { + Vanilla.MinecraftServer_getScriptServer(server).reAddCommands(); + } +} diff --git a/src/main/java/carpet/mixins/PlayerTabOverlayMixin.java b/src/main/java/carpet/mixins/PlayerTabOverlayMixin.java new file mode 100644 index 0000000..7fe5020 --- /dev/null +++ b/src/main/java/carpet/mixins/PlayerTabOverlayMixin.java @@ -0,0 +1,20 @@ +package carpet.mixins; +import carpet.fakes.PlayerListHudInterface; +import net.minecraft.client.gui.components.PlayerTabOverlay; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(PlayerTabOverlay.class) +public abstract class PlayerTabOverlayMixin implements PlayerListHudInterface +{ + @Shadow private Component footer; + + @Shadow private Component header; + + @Override + public boolean hasFooterOrHeader() + { + return footer != null || header != null; + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/Player_antiCheatDisabledMixin.java b/src/main/java/carpet/mixins/Player_antiCheatDisabledMixin.java new file mode 100644 index 0000000..8a9625d --- /dev/null +++ b/src/main/java/carpet/mixins/Player_antiCheatDisabledMixin.java @@ -0,0 +1,35 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ElytraItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Player.class) +public abstract class Player_antiCheatDisabledMixin +{ + @Shadow public abstract ItemStack getItemBySlot(EquipmentSlot equipmentSlot_1); + + @Shadow public abstract void startFallFlying(); + + @Inject(method = "tryToStartFallFlying", at = @At("HEAD"), cancellable = true) + private void allowDeploys(CallbackInfoReturnable cir) + { + if (CarpetSettings.antiCheatDisabled && (Object)this instanceof ServerPlayer sp && sp.getServer().isDedicatedServer()) + { + ItemStack itemStack_1 = getItemBySlot(EquipmentSlot.CHEST); + if (itemStack_1.getItem() == Items.ELYTRA && ElytraItem.isFlyEnabled(itemStack_1)) { + startFallFlying(); + cir.setReturnValue(true); + } + } + } +} diff --git a/src/main/java/carpet/mixins/Player_creativeNoClipMixin.java b/src/main/java/carpet/mixins/Player_creativeNoClipMixin.java new file mode 100644 index 0000000..055f8e5 --- /dev/null +++ b/src/main/java/carpet/mixins/Player_creativeNoClipMixin.java @@ -0,0 +1,47 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Player.class) +public abstract class Player_creativeNoClipMixin extends LivingEntity +{ + protected Player_creativeNoClipMixin(EntityType type, Level world) + { + super(type, world); + } + + @Redirect(method = "tick", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;isSpectator()Z") + ) + private boolean canClipTroughWorld(Player playerEntity) + { + return playerEntity.isSpectator() || (CarpetSettings.creativeNoClip && playerEntity.isCreative() && playerEntity.getAbilities().flying); + + } + + @Redirect(method = "aiStep", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;isSpectator()Z") + ) + private boolean collidesWithEntities(Player playerEntity) + { + return playerEntity.isSpectator() || (CarpetSettings.creativeNoClip && playerEntity.isCreative() && playerEntity.getAbilities().flying); + } + + @Redirect(method = "updatePlayerPose", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;isSpectator()Z") + ) + private boolean spectatorsDontPose(Player playerEntity) + { + return playerEntity.isSpectator() || (CarpetSettings.creativeNoClip && playerEntity.isCreative() && playerEntity.getAbilities().flying); + } +} diff --git a/src/main/java/carpet/mixins/Player_fakePlayersMixin.java b/src/main/java/carpet/mixins/Player_fakePlayersMixin.java new file mode 100644 index 0000000..5004a84 --- /dev/null +++ b/src/main/java/carpet/mixins/Player_fakePlayersMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.patches.EntityPlayerMPFake; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Player.class) +public abstract class Player_fakePlayersMixin +{ + /** + * To make sure player attacks are able to knockback fake players + */ + @Redirect( + method = "attack", + at = @At( + value = "FIELD", + target = "Lnet/minecraft/world/entity/Entity;hurtMarked:Z", + ordinal = 0 + ) + ) + private boolean velocityModifiedAndNotCarpetFakePlayer(Entity target) + { + return target.hurtMarked && !(target instanceof EntityPlayerMPFake); + } +} diff --git a/src/main/java/carpet/mixins/Player_parrotMixin.java b/src/main/java/carpet/mixins/Player_parrotMixin.java new file mode 100644 index 0000000..bc29fc6 --- /dev/null +++ b/src/main/java/carpet/mixins/Player_parrotMixin.java @@ -0,0 +1,95 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Abilities; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Player.class) +public abstract class Player_parrotMixin extends LivingEntity +{ + @Shadow @Final public Abilities abilities; + + @Shadow protected abstract void removeEntitiesOnShoulder(); + + @Shadow public abstract CompoundTag getShoulderEntityLeft(); + + @Shadow protected abstract void setShoulderEntityLeft(CompoundTag NbtCompound_1); + + @Shadow protected abstract void setShoulderEntityRight(CompoundTag NbtCompound_1); + + @Shadow public abstract CompoundTag getShoulderEntityRight(); + + @Shadow protected abstract void respawnEntityOnShoulder(CompoundTag entityNbt); + + protected Player_parrotMixin(EntityType entityType_1, Level world_1) + { + super(entityType_1, world_1); + } + + @Redirect(method = "aiStep", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;removeEntitiesOnShoulder()V")) + private void cancelDropShoulderEntities1(Player playerEntity) + { + + } + + @Inject(method = "aiStep", at = @At(value = "INVOKE", shift = At.Shift.AFTER, ordinal = 1, + target = "Lnet/minecraft/world/entity/player/Player;playShoulderEntityAmbientSound(Lnet/minecraft/nbt/CompoundTag;)V")) + private void onTickMovement(CallbackInfo ci) + { + boolean parrots_will_drop = !CarpetSettings.persistentParrots || this.abilities.invulnerable; + if (!this.level().isClientSide && ((parrots_will_drop && this.fallDistance > 0.5F) || this.isInWater() || this.abilities.flying || isSleeping())) + { + this.removeEntitiesOnShoulder(); + } + } + + @Redirect(method = "hurt", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;removeEntitiesOnShoulder()V")) + private void cancelDropShoulderEntities2(Player playerEntity) + { + + } + + protected void dismount_left() + { + respawnEntityOnShoulder(this.getShoulderEntityLeft()); + this.setShoulderEntityLeft(new CompoundTag()); + } + + protected void dismount_right() + { + respawnEntityOnShoulder(this.getShoulderEntityRight()); + this.setShoulderEntityRight(new CompoundTag()); + } + + @Inject(method = "hurt", at = @At(value = "INVOKE", shift = At.Shift.BEFORE, + target = "Lnet/minecraft/world/entity/player/Player;removeEntitiesOnShoulder()V")) + private void onDamage(DamageSource damageSource_1, float float_1, CallbackInfoReturnable cir) + { + if (CarpetSettings.persistentParrots && !this.isShiftKeyDown()) + { + if (this.random.nextFloat() < ((float_1)/15.0) ) + { + this.dismount_left(); + } + if (this.random.nextFloat() < ((float_1)/15.0) ) + { + this.dismount_right(); + } + } + } +} diff --git a/src/main/java/carpet/mixins/Player_scarpetEventsMixin.java b/src/main/java/carpet/mixins/Player_scarpetEventsMixin.java new file mode 100644 index 0000000..3172026 --- /dev/null +++ b/src/main/java/carpet/mixins/Player_scarpetEventsMixin.java @@ -0,0 +1,88 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import static carpet.script.CarpetEventServer.Event.PLAYER_ATTACKS_ENTITY; +import static carpet.script.CarpetEventServer.Event.PLAYER_DEALS_DAMAGE; +import static carpet.script.CarpetEventServer.Event.PLAYER_INTERACTS_WITH_ENTITY; +import static carpet.script.CarpetEventServer.Event.PLAYER_TAKES_DAMAGE; +import static carpet.script.CarpetEventServer.Event.PLAYER_COLLIDES_WITH_ENTITY; + +@Mixin(Player.class) +public abstract class Player_scarpetEventsMixin extends LivingEntity +{ + protected Player_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "actuallyHurt", cancellable = true, locals = LocalCapture.CAPTURE_FAILHARD, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getDamageAfterArmorAbsorb(Lnet/minecraft/world/damagesource/DamageSource;F)F" + )) + private void playerTakingDamage(DamageSource source, float amount, CallbackInfo ci) + { + // version of LivingEntity_scarpetEventsMixin::entityTakingDamage + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_DAMAGE, amount, source); + if (PLAYER_TAKES_DAMAGE.isNeeded()) + { + if(PLAYER_TAKES_DAMAGE.onDamage(this, amount, source)) { + ci.cancel(); + } + } + if (source.getEntity() instanceof ServerPlayer && PLAYER_DEALS_DAMAGE.isNeeded()) + { + if(PLAYER_DEALS_DAMAGE.onDamage(this, amount, source)) { + ci.cancel(); + } + } + } + + @Inject(method = "touch", at = @At("HEAD")) + private void onEntityCollision(Entity entity, CallbackInfo ci) + { + if (PLAYER_COLLIDES_WITH_ENTITY.isNeeded() && !level().isClientSide) + { + PLAYER_COLLIDES_WITH_ENTITY.onEntityHandAction((ServerPlayer)(Object)this, entity, null); + } + } + + @Inject(method = "interactOn", cancellable = true, at = @At("HEAD")) + private void doInteract(Entity entity, InteractionHand hand, CallbackInfoReturnable cir) + { + if (!level().isClientSide && PLAYER_INTERACTS_WITH_ENTITY.isNeeded()) + { + if(PLAYER_INTERACTS_WITH_ENTITY.onEntityHandAction((ServerPlayer) (Object)this, entity, hand)) { + cir.setReturnValue(InteractionResult.PASS); + cir.cancel(); + } + } + } + + @Inject(method = "attack", at = @At("HEAD"), cancellable = true) + private void onAttack(Entity target, CallbackInfo ci) + { + if (!level().isClientSide && PLAYER_ATTACKS_ENTITY.isNeeded() && target.isAttackable()) + { + if(PLAYER_ATTACKS_ENTITY.onEntityHandAction((ServerPlayer) (Object)this, target, null)) { + ci.cancel(); + } + } + } +} diff --git a/src/main/java/carpet/mixins/Player_xpNoCooldownMixin.java b/src/main/java/carpet/mixins/Player_xpNoCooldownMixin.java new file mode 100644 index 0000000..0cd8e00 --- /dev/null +++ b/src/main/java/carpet/mixins/Player_xpNoCooldownMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +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; + +import java.util.List; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; + +@Mixin(Player.class) +public abstract class Player_xpNoCooldownMixin { + + @Shadow + protected abstract void touch(Entity entity); + + @Redirect(method = "aiStep",at = @At(value = "INVOKE", target = "java/util/List.add(Ljava/lang/Object;)Z")) + public boolean processXpOrbCollisions(List instance, Object e) { + Entity entity = (Entity) e; + if (CarpetSettings.xpNoCooldown) { + this.touch(entity); + return true; + } + return instance.add(entity); + } +} diff --git a/src/main/java/carpet/mixins/PoiRecord_scarpetMixin.java b/src/main/java/carpet/mixins/PoiRecord_scarpetMixin.java new file mode 100644 index 0000000..72466dc --- /dev/null +++ b/src/main/java/carpet/mixins/PoiRecord_scarpetMixin.java @@ -0,0 +1,16 @@ +package carpet.mixins; + +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(PoiRecord.class) +public interface PoiRecord_scarpetMixin +{ + @Accessor("freeTickets") + int getFreeTickets(); + + @Invoker + boolean callAcquireTicket(); +} diff --git a/src/main/java/carpet/mixins/PortalProcessor_scarpetMixin.java b/src/main/java/carpet/mixins/PortalProcessor_scarpetMixin.java new file mode 100644 index 0000000..197a497 --- /dev/null +++ b/src/main/java/carpet/mixins/PortalProcessor_scarpetMixin.java @@ -0,0 +1,19 @@ +package carpet.mixins; + +import carpet.fakes.PortalProcessorInterface; +import net.minecraft.world.entity.PortalProcessor; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(PortalProcessor.class) +public class PortalProcessor_scarpetMixin implements PortalProcessorInterface +{ + + @Shadow private int portalTime; + + @Override + public void setPortalTime(int time) + { + portalTime = time; + } +} diff --git a/src/main/java/carpet/mixins/PoweredRailBlock_powerLimitMixin.java b/src/main/java/carpet/mixins/PoweredRailBlock_powerLimitMixin.java new file mode 100644 index 0000000..fe3e678 --- /dev/null +++ b/src/main/java/carpet/mixins/PoweredRailBlock_powerLimitMixin.java @@ -0,0 +1,18 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.level.block.PoweredRailBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(PoweredRailBlock.class) +public class PoweredRailBlock_powerLimitMixin +{ + @ModifyConstant(method = "findPoweredRailSignal(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;ZI)Z", + constant = @Constant(intValue = 8)) + private int powerLimit(int original) + { + return CarpetSettings.railPowerLimit-1; + } +} diff --git a/src/main/java/carpet/mixins/PrimedTntMixin.java b/src/main/java/carpet/mixins/PrimedTntMixin.java new file mode 100644 index 0000000..0328ace --- /dev/null +++ b/src/main/java/carpet/mixins/PrimedTntMixin.java @@ -0,0 +1,125 @@ +package carpet.mixins; + +import carpet.fakes.TntEntityInterface; +import carpet.CarpetSettings; +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; +import carpet.logging.LoggerRegistry; +import carpet.logging.logHelpers.TNTLogHelper; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +@Mixin(PrimedTnt.class) +public abstract class PrimedTntMixin extends Entity implements TntEntityInterface +{ + @Shadow public abstract int getFuse(); + + private TNTLogHelper logHelper; + private boolean mergeBool = false; + private int mergedTNT = 1; + + public PrimedTntMixin(EntityType entityType_1, Level world_1) + { + super(entityType_1, world_1); + } + + + @Inject(method = "(Lnet/minecraft/world/level/Level;DDDLnet/minecraft/world/entity/LivingEntity;)V", + at = @At("RETURN")) + private void modifyTNTAngle(Level world, double x, double y, double z, LivingEntity entity, CallbackInfo ci) + { + if (CarpetSettings.hardcodeTNTangle != -1.0D) + setDeltaMovement(-Math.sin(CarpetSettings.hardcodeTNTangle) * 0.02, 0.2, -Math.cos(CarpetSettings.hardcodeTNTangle) * 0.02); + } + + @Inject(method = "(Lnet/minecraft/world/entity/EntityType;Lnet/minecraft/world/level/Level;)V", at = @At("RETURN")) + private void initTNTLoggerPrime(EntityType entityType_1, Level world_1, CallbackInfo ci) + { + if (LoggerRegistry.__tnt && !world_1.isClientSide) + { + logHelper = new TNTLogHelper(); + } + } + + @Inject(method = "tick", at = @At("HEAD")) + private void initTracker(CallbackInfo ci) + { + if (LoggerRegistry.__tnt && logHelper != null && !logHelper.initialized) + { + logHelper.onPrimed(getX(), getY(), getZ(), getDeltaMovement()); + } + } + + + @Inject(method = "(Lnet/minecraft/world/level/Level;DDDLnet/minecraft/world/entity/LivingEntity;)V", + at = @At(value = "RETURN")) + private void initTNTLogger(Level world_1, double double_1, double double_2, double double_3, + LivingEntity livingEntity_1, CallbackInfo ci) + { + if(CarpetSettings.tntPrimerMomentumRemoved) + this.setDeltaMovement(new Vec3(0.0, 0.20000000298023224D, 0.0)); + } + + @Inject(method = "explode", at = @At(value = "HEAD")) + private void onExplode(CallbackInfo ci) + { + if (LoggerRegistry.__tnt && logHelper != null) + logHelper.onExploded(getX(), getY(), getZ(), this.level().getGameTime()); + + if (mergedTNT > 1) + for (int i = 0; i < mergedTNT - 1; i++) + this.level().explode(this, this.getX(), this.getY() + (double)(this.getBbHeight() / 16.0F), + this.getZ(), + 4.0F, + Level.ExplosionInteraction.TNT); + } + + @Inject(method = "tick", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/entity/item/PrimedTnt;setDeltaMovement(Lnet/minecraft/world/phys/Vec3;)V", + ordinal = 1)) + private void tryMergeTnt(CallbackInfo ci) + { + // Merge code for combining tnt into a single entity if they happen to exist in the same spot, same fuse, no motion CARPET-XCOM + if(CarpetSettings.mergeTNT){ + Vec3 velocity = getDeltaMovement(); + if(!level().isClientSide && mergeBool && velocity.x == 0 && velocity.y == 0 && velocity.z == 0){ + mergeBool = false; + for(Entity entity : level().getEntities(this, this.getBoundingBox())){ + if(entity instanceof PrimedTnt && !entity.isRemoved()){ + PrimedTnt entityTNTPrimed = (PrimedTnt)entity; + Vec3 tntVelocity = entityTNTPrimed.getDeltaMovement(); + if(tntVelocity.x == 0 && tntVelocity.y == 0 && tntVelocity.z == 0 + && this.getX() == entityTNTPrimed.getX() && this.getZ() == entityTNTPrimed.getZ() && this.getY() == entityTNTPrimed.getY() + && getFuse() == entityTNTPrimed.getFuse()){ + mergedTNT += ((TntEntityInterface) entityTNTPrimed).getMergedTNT(); + entityTNTPrimed.discard(); // discard remove(); + } + } + } + } + } + } + + @Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/item/PrimedTnt;setFuse(I)V")) + private void setMergeable(CallbackInfo ci) + { + // Merge code, merge only tnt that have had a chance to move CARPET-XCOM + Vec3 velocity = getDeltaMovement(); + if(!level().isClientSide && (velocity.y != 0 || velocity.x != 0 || velocity.z != 0)){ + mergeBool = true; + } + } + + @Override + public int getMergedTNT() { + return mergedTNT; + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/PrimedTnt_scarpetEventsMixin.java b/src/main/java/carpet/mixins/PrimedTnt_scarpetEventsMixin.java new file mode 100644 index 0000000..591f8d9 --- /dev/null +++ b/src/main/java/carpet/mixins/PrimedTnt_scarpetEventsMixin.java @@ -0,0 +1,28 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.script.EntityEventsGroup; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.level.Level; +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(PrimedTnt.class) +public abstract class PrimedTnt_scarpetEventsMixin extends Entity +{ + public PrimedTnt_scarpetEventsMixin(EntityType type, Level world) + { + super(type, world); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void onTickCall(CallbackInfo ci) + { + // calling extra on_tick because falling blocks do not fall back to super tick call + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_TICK); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/RandomState_ScarpetMixin.java b/src/main/java/carpet/mixins/RandomState_ScarpetMixin.java new file mode 100644 index 0000000..2e85523 --- /dev/null +++ b/src/main/java/carpet/mixins/RandomState_ScarpetMixin.java @@ -0,0 +1,33 @@ +package carpet.mixins; + +import carpet.fakes.RandomStateVisitorAccessor; +import net.minecraft.world.level.levelgen.DensityFunction; +import net.minecraft.world.level.levelgen.RandomState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +@Mixin(RandomState.class) +public class RandomState_ScarpetMixin implements RandomStateVisitorAccessor { + @Unique + private DensityFunction.Visitor visitor; + + @ModifyArg( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/NoiseRouter;mapAll(Lnet/minecraft/world/level/levelgen/DensityFunction$Visitor;)Lnet/minecraft/world/level/levelgen/NoiseRouter;" + ), + index = 0 + ) + private DensityFunction.Visitor captureVisitor(DensityFunction.Visitor visitor) { + this.visitor = visitor; + return visitor; + } + + @Override + public DensityFunction.Visitor getVisitor() { + return this.visitor; + } +} diff --git a/src/main/java/carpet/mixins/RecipeBookMenu_scarpetMixin.java b/src/main/java/carpet/mixins/RecipeBookMenu_scarpetMixin.java new file mode 100644 index 0000000..e28072b --- /dev/null +++ b/src/main/java/carpet/mixins/RecipeBookMenu_scarpetMixin.java @@ -0,0 +1,19 @@ +package carpet.mixins; + +import carpet.fakes.AbstractContainerMenuInterface; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.RecipeBookMenu; +import net.minecraft.world.item.crafting.RecipeHolder; +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(RecipeBookMenu.class) +public class RecipeBookMenu_scarpetMixin { + @Inject(method = "handlePlacement",at = @At("HEAD"), cancellable = true) + private void selectRecipeCallback(boolean craftAll, RecipeHolder recipe, ServerPlayer player, CallbackInfo ci) { + if(((AbstractContainerMenuInterface) this).callSelectRecipeListener(player,recipe,craftAll)) + ci.cancel(); + } +} diff --git a/src/main/java/carpet/mixins/RecipeManager_scarpetMixin.java b/src/main/java/carpet/mixins/RecipeManager_scarpetMixin.java new file mode 100644 index 0000000..d95593e --- /dev/null +++ b/src/main/java/carpet/mixins/RecipeManager_scarpetMixin.java @@ -0,0 +1,50 @@ +package carpet.mixins; + +import carpet.fakes.RecipeManagerInterface; +import com.google.common.collect.Multimap; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.Item; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; + +@Mixin(RecipeManager.class) +public class RecipeManager_scarpetMixin implements RecipeManagerInterface +{ + @Shadow private Multimap, RecipeHolder> byType; + + @Shadow private Map> byName; + + @Override + public List> getAllMatching(RecipeType type, ResourceLocation itemId, RegistryAccess registryAccess) + { + // quiq cheq + RecipeHolder recipe = byName.get(itemId); + if (recipe != null && recipe.value().getType().equals(type)) + { + return List.of(recipe.value()); + } + if (!byType.containsKey(type)) + { + // happens when mods add recipe to the registry without updating recipe manager + return List.of(); + } + Collection> typeRecipes = byType.get(type); + Registry regs = registryAccess.registryOrThrow(Registries.ITEM); + Item item = regs.get(itemId); + return typeRecipes.stream() + .>map(RecipeHolder::value) + .filter(r -> r.getResultItem(registryAccess).getItem() == item) + .toList(); + } +} diff --git a/src/main/java/carpet/mixins/RedstoneWireBlock_fastMixin.java b/src/main/java/carpet/mixins/RedstoneWireBlock_fastMixin.java new file mode 100644 index 0000000..0ac25bd --- /dev/null +++ b/src/main/java/carpet/mixins/RedstoneWireBlock_fastMixin.java @@ -0,0 +1,134 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.fakes.RedstoneWireBlockInterface; +import carpet.helpers.RedstoneWireTurbo; +import com.google.common.collect.Sets; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Accessor; +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 java.util.Set; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RedStoneWireBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; + +import static net.minecraft.world.level.block.RedStoneWireBlock.POWER; + +@Mixin(RedStoneWireBlock.class) +public abstract class RedstoneWireBlock_fastMixin implements RedstoneWireBlockInterface { + + @Shadow + private void updatePowerStrength(Level world_1, BlockPos blockPos_1, BlockState blockState_1) { } + + @Shadow + private int calculateTargetStrength(Level world, BlockPos pos) { return 0; } + + @Override + @Accessor("shouldSignal") + public abstract void setWiresGivePower(boolean wiresGivePower); + + @Override + @Accessor("shouldSignal") + public abstract boolean getWiresGivePower(); + + // = + + private RedstoneWireTurbo wireTurbo = null; + + @Inject(method = "", at = @At("RETURN")) + private void onRedstoneWireBlockCTOR(BlockBehaviour.Properties settings, CallbackInfo ci) { + //noinspection ConstantConditions + wireTurbo = new RedstoneWireTurbo((RedStoneWireBlock) (Object) this); + } + + // = + + public void fastUpdate(Level world, BlockPos pos, BlockState state, BlockPos source) { + // [CM] fastRedstoneDust -- update based on carpet rule + if (CarpetSettings.fastRedstoneDust) { + wireTurbo.updateSurroundingRedstone(world, pos, state, source); + return; + } + updatePowerStrength(world, pos, state); + } + + /** + * @author theosib, soykaf, gnembon + */ + @Inject(method = "updatePowerStrength", at = @At("HEAD"), cancellable = true) + private void updateLogicAlternative(Level world, BlockPos pos, BlockState state, CallbackInfo cir) { + if (CarpetSettings.fastRedstoneDust) { + updateLogicPublic(world, pos, state); + cir.cancel(); + } + } + + @Override + public BlockState updateLogicPublic(Level world_1, BlockPos blockPos_1, BlockState blockState_1) { + int i = this.calculateTargetStrength(world_1, blockPos_1); + BlockState blockState = blockState_1; + if (blockState_1.getValue(POWER) != i) { + blockState_1 = blockState_1.setValue(POWER, i); + if (world_1.getBlockState(blockPos_1) == blockState) { + // [Space Walker] suppress shape updates and emit those manually to + // bypass the new neighbor update stack. + if (world_1.setBlock(blockPos_1, blockState_1, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) + wireTurbo.updateNeighborShapes(world_1, blockPos_1, blockState_1); + } + + if (!CarpetSettings.fastRedstoneDust) { + Set set = Sets.newHashSet(); + set.add(blockPos_1); + Direction[] var6 = Direction.values(); + int var7 = var6.length; + + for (int var8 = 0; var8 < var7; ++var8) { + Direction direction = var6[var8]; + set.add(blockPos_1.relative(direction)); + } + + for (BlockPos blockPos : set) { + world_1.updateNeighborsAt(blockPos, blockState_1.getBlock()); + } + } + } + return blockState_1; + } + + // = + + + @Redirect(method = "onPlace", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/RedStoneWireBlock;updatePowerStrength(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V")) + private void redirectOnBlockAddedUpdate(RedStoneWireBlock self, Level world_1, BlockPos blockPos_1, BlockState blockState_1) { + fastUpdate(world_1, blockPos_1, blockState_1, null); + } + + @Redirect(method = "onRemove", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/RedStoneWireBlock;updatePowerStrength(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V")) + private void redirectOnStateReplacedUpdate(RedStoneWireBlock self, Level world_1, BlockPos blockPos_1, BlockState blockState_1) { + fastUpdate(world_1, blockPos_1, blockState_1, null); + } + + @Redirect(method = "neighborChanged", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/RedStoneWireBlock;updatePowerStrength(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;)V")) + private void redirectNeighborUpdateUpdate( + RedStoneWireBlock self, + Level world_1, + BlockPos blockPos_1, + BlockState blockState_1, + BlockState blockState_2, + Level world_2, + BlockPos blockPos_2, + Block block_1, + BlockPos blockPos_3, + boolean boolean_1) { + fastUpdate(world_1, blockPos_1, blockState_1, blockPos_3); + } +} diff --git a/src/main/java/carpet/mixins/ReloadCommand_reloadAppsMixin.java b/src/main/java/carpet/mixins/ReloadCommand_reloadAppsMixin.java new file mode 100644 index 0000000..f27abc7 --- /dev/null +++ b/src/main/java/carpet/mixins/ReloadCommand_reloadAppsMixin.java @@ -0,0 +1,22 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.commands.ReloadCommand; +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. + @Inject(method = "method_13530", at = @At("TAIL"), remap = false) + private static void onReload(CommandContext context, CallbackInfoReturnable cir) + { + // can't fetch here the reference to the server + CarpetServer.onReload(context.getSource().getServer()); + } +} diff --git a/src/main/java/carpet/mixins/SaplingBlock_desertShrubsMixin.java b/src/main/java/carpet/mixins/SaplingBlock_desertShrubsMixin.java new file mode 100644 index 0000000..140829c --- /dev/null +++ b/src/main/java/carpet/mixins/SaplingBlock_desertShrubsMixin.java @@ -0,0 +1,50 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.util.RandomSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Random; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BiomeTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SaplingBlock; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(SaplingBlock.class) +public abstract class SaplingBlock_desertShrubsMixin +{ + @Inject(method = "advanceTree", at = @At(value = "INVOKE", shift = At.Shift.BEFORE, + target = "Lnet/minecraft/world/level/block/grower/TreeGrower;growTree(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/util/RandomSource;)Z"), + cancellable = true) + private void onGenerate(ServerLevel level, BlockPos pos, BlockState blockState, RandomSource random, CallbackInfo ci) + { + if (CarpetSettings.desertShrubs && level.getBiome(pos).is(BiomeTags.HAS_DESERT_PYRAMID) && !nearWater(level, pos)) + { + level.setBlock(pos, Blocks.DEAD_BUSH.defaultBlockState(), Block.UPDATE_ALL); + ci.cancel(); + } + } + + @Unique + private static boolean nearWater(LevelAccessor level, BlockPos pos) + { + for (BlockPos blockPos : BlockPos.betweenClosed(pos.offset(-4, -4, -4), pos.offset(4, 1, 4))) + { + if (level.getFluidState(blockPos).is(FluidTags.WATER)) + { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/carpet/mixins/Scoreboard_scarpetMixin.java b/src/main/java/carpet/mixins/Scoreboard_scarpetMixin.java new file mode 100644 index 0000000..71ddebd --- /dev/null +++ b/src/main/java/carpet/mixins/Scoreboard_scarpetMixin.java @@ -0,0 +1,16 @@ +package carpet.mixins; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; + +@Mixin(Scoreboard.class) +public interface Scoreboard_scarpetMixin { + @Accessor("objectivesByCriteria") + Reference2ObjectMap> getObjectivesByCriterion(); +} diff --git a/src/main/java/carpet/mixins/SculkSensorBlockEntityVibrationConfig_sculkSensorRangeMixin.java b/src/main/java/carpet/mixins/SculkSensorBlockEntityVibrationConfig_sculkSensorRangeMixin.java new file mode 100644 index 0000000..149f52b --- /dev/null +++ b/src/main/java/carpet/mixins/SculkSensorBlockEntityVibrationConfig_sculkSensorRangeMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + + +import carpet.CarpetSettings; +import net.minecraft.world.level.block.entity.SculkSensorBlockEntity; +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(SculkSensorBlockEntity.VibrationUser.class) +public class SculkSensorBlockEntityVibrationConfig_sculkSensorRangeMixin +{ + @Inject(method = "getListenerRadius", at = @At("HEAD"), cancellable = true) + private void sculkSensorRange(CallbackInfoReturnable cir) + { + if (CarpetSettings.sculkSensorRange != SculkSensorBlockEntity.VibrationUser.LISTENER_RANGE) { + cir.setReturnValue(CarpetSettings.sculkSensorRange); + } + } +} diff --git a/src/main/java/carpet/mixins/ServerChunkCacheMixin.java b/src/main/java/carpet/mixins/ServerChunkCacheMixin.java new file mode 100644 index 0000000..815ed55 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerChunkCacheMixin.java @@ -0,0 +1,101 @@ +package carpet.mixins; + +import carpet.utils.SpawnReporter; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import org.apache.commons.lang3.tuple.Pair; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.HashSet; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.LevelData; + +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCacheMixin +{ + @Shadow @Final private ServerLevel level; + + @Shadow @Final private DistanceManager distanceManager; + + @Redirect(method = "tickChunks", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/DistanceManager;getNaturalSpawnChunkCount()I" + )) + //this runs once per world spawning cycle. Allows to grab mob counts and count spawn ticks + private int setupTracking(DistanceManager chunkTicketManager) + { + int j = chunkTicketManager.getNaturalSpawnChunkCount(); + ResourceKey dim = this.level.dimension(); // getDimensionType; + //((WorldInterface)world).getPrecookedMobs().clear(); not needed because mobs are compared with predefined BBs + SpawnReporter.chunkCounts.put(dim, j); + + if (SpawnReporter.trackingSpawns()) + { + //local spawns now need to be tracked globally cause each calll is just for chunk + SpawnReporter.local_spawns = new Object2LongOpenHashMap<>(); + SpawnReporter.first_chunk_marker = new HashSet<>(); + for (MobCategory cat : SpawnReporter.cachedMobCategories()) + { + Pair, MobCategory> key = Pair.of(dim, cat); + SpawnReporter.overall_spawn_ticks.addTo(key, SpawnReporter.spawn_tries.get(cat)); + } + } + return j; + } + + + @Inject(method = "tickChunks", at = @At("RETURN")) + private void onFinishSpawnWorldCycle(CallbackInfo ci) + { + LevelData levelData = this.level.getLevelData(); // levelProperies class + boolean boolean_3 = levelData.getGameTime() % 400L == 0L; + if (SpawnReporter.trackingSpawns() && SpawnReporter.local_spawns != null) + { + for (MobCategory cat: SpawnReporter.cachedMobCategories()) + { + ResourceKey dim = level.dimension(); // getDimensionType; + Pair, MobCategory> key = Pair.of(dim, cat); + int spawnTries = SpawnReporter.spawn_tries.get(cat); + if (!SpawnReporter.local_spawns.containsKey(cat)) + { + if (!cat.isPersistent() || boolean_3) // isAnimal + { + // fill mobcaps for that category so spawn got cancelled + SpawnReporter.spawn_ticks_full.addTo(key, spawnTries); + } + + } + else if (SpawnReporter.local_spawns.getLong(cat) > 0) + { + // tick spawned mobs for that type + SpawnReporter.spawn_ticks_succ.addTo(key, spawnTries); + SpawnReporter.spawn_ticks_spawns.addTo(key, SpawnReporter.local_spawns.getLong(cat)); + // this will be off comparing to 1.13 as that would succeed if + // ANY tries in that round were successful. + // there will be much more difficult to mix in + // considering spawn tries to remove, as with warp + // there is little need for them anyways. + } + else // spawn no mobs despite trying + { + //tick didn's spawn mobs of that type + SpawnReporter.spawn_ticks_fail.addTo(key, spawnTries); + } + } + } + SpawnReporter.local_spawns = null; + } + + + +} diff --git a/src/main/java/carpet/mixins/ServerChunkCache_profilerMixin.java b/src/main/java/carpet/mixins/ServerChunkCache_profilerMixin.java new file mode 100644 index 0000000..6dabe40 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerChunkCache_profilerMixin.java @@ -0,0 +1,73 @@ +package carpet.mixins; + +import carpet.utils.CarpetProfiler; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; + +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCache_profilerMixin +{ + + @Shadow @Final ServerLevel level; + + CarpetProfiler.ProfilerToken currentSection; + + @Inject(method = "tickChunks", at = @At("HEAD")) + private void startSpawningSection(CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(level, "Spawning", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tickChunks", at = @At( + value = "FIELD", + target = "net/minecraft/server/level/ServerChunkCache.level:Lnet/minecraft/server/level/ServerLevel;", + ordinal = 10 + )) + private void skipChunkTicking(CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + } + } + + @Inject(method = "tickChunks", at = @At( + value = "INVOKE", + target = "net/minecraft/server/level/ServerLevel.tickChunk(Lnet/minecraft/world/level/chunk/LevelChunk;I)V", + shift = At.Shift.AFTER + )) + private void resumeSpawningSection(CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(level, "Spawning", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tickChunks", at = @At("RETURN")) + private void stopSpawningSection(CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + } + } + + //@Redirect(method = "tick", at = @At( + // value = "INVOKE", + // target = "Lnet/minecraft/server/level/DistanceManager;purgeStaleTickets()V" + //)) + //private void pauseTicketSystem(DistanceManager distanceManager) + //{ + // pausing expiry of tickets + // that will prevent also chunks from unloading, so require a deep frozen state + //ServerTickRateManager trm = ((MinecraftServerInterface) level.getServer()).getTickRateManager(); + //if (!trm.runsNormally() && trm.deeplyFrozen()) return; + //distanceManager.purgeStaleTickets(); + //} + +} diff --git a/src/main/java/carpet/mixins/ServerFunctionManager_profilerMixin.java b/src/main/java/carpet/mixins/ServerFunctionManager_profilerMixin.java new file mode 100644 index 0000000..7451507 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerFunctionManager_profilerMixin.java @@ -0,0 +1,26 @@ +package carpet.mixins; + +import carpet.utils.CarpetProfiler; +import net.minecraft.server.ServerFunctionManager; +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(ServerFunctionManager.class) +public class ServerFunctionManager_profilerMixin +{ + CarpetProfiler.ProfilerToken currentSection; + + @Inject(method = "tick", at = @At("HEAD"), cancellable = true) + private void beforeDatapacks(CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section(null, "Datapacks", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tick", at = @At("RETURN")) + private void afterDatapacks(CallbackInfo ci) + { + CarpetProfiler.end_current_section(currentSection); + } +} diff --git a/src/main/java/carpet/mixins/ServerGamePacketListenerImplMixin.java b/src/main/java/carpet/mixins/ServerGamePacketListenerImplMixin.java new file mode 100644 index 0000000..06aaa47 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerGamePacketListenerImplMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.network.CarpetClient; +import carpet.network.ServerNetworkHandler; +import net.minecraft.network.protocol.PacketUtils; +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; +import net.minecraft.network.protocol.game.ServerGamePacketListener; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +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(ServerGamePacketListenerImpl.class) +public class ServerGamePacketListenerImplMixin +{ + @Shadow public ServerPlayer player; + + +} diff --git a/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_antiCheatDisabledMixin.java b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_antiCheatDisabledMixin.java new file mode 100644 index 0000000..3aed917 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_antiCheatDisabledMixin.java @@ -0,0 +1,59 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +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; + +@Mixin(ServerGamePacketListenerImpl.class) +public abstract class ServerGamePacketListenerImpl_antiCheatDisabledMixin extends ServerCommonPacketListenerImpl +{ + @Shadow private int aboveGroundTickCount; + + @Shadow private int aboveGroundVehicleTickCount; + + public ServerGamePacketListenerImpl_antiCheatDisabledMixin(final MinecraftServer minecraftServer, final Connection connection, CommonListenerCookie cci) + { + super(minecraftServer, connection, cci); + } + + //@Shadow protected abstract boolean isSingleplayerOwner(); + + @Inject(method = "tick", at = @At("HEAD")) + private void restrictFloatingBits(CallbackInfo ci) + { + if (CarpetSettings.antiCheatDisabled) + { + if (aboveGroundTickCount > 70) aboveGroundTickCount--; + if (aboveGroundVehicleTickCount > 70) aboveGroundVehicleTickCount--; + } + + } + + @Redirect(method = "handleMoveVehicle", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/network/ServerGamePacketListenerImpl;isSingleplayerOwner()Z" + )) + private boolean isServerTrusting(ServerGamePacketListenerImpl serverPlayNetworkHandler) + { + return isSingleplayerOwner() || CarpetSettings.antiCheatDisabled; + } + + @Redirect(method = "handleMovePlayer", require = 0, // don't crash with immersive portals, + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;isChangingDimension()Z")) + private boolean relaxMoveRestrictions(ServerPlayer serverPlayerEntity) + { + return CarpetSettings.antiCheatDisabled || serverPlayerEntity.isChangingDimension(); + } +} diff --git a/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_coreMixin.java b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_coreMixin.java new file mode 100644 index 0000000..b0ddb00 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_coreMixin.java @@ -0,0 +1,38 @@ +package carpet.mixins; + +import carpet.CarpetServer; +import carpet.fakes.ServerGamePacketListenerImplInterface; +import net.minecraft.network.Connection; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +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(ServerGamePacketListenerImpl.class) +public abstract class ServerGamePacketListenerImpl_coreMixin extends ServerCommonPacketListenerImpl implements ServerGamePacketListenerImplInterface { + @Shadow + public ServerPlayer player; + + public ServerGamePacketListenerImpl_coreMixin(final MinecraftServer minecraftServer, final Connection connection, final CommonListenerCookie i) + { + super(minecraftServer, connection, i); + } + + @Inject(method = "onDisconnect", at = @At("HEAD")) + private void onPlayerDisconnect(DisconnectionDetails reason, CallbackInfo ci) { + CarpetServer.onPlayerLoggedOut(this.player, reason.reason()); + } + + @Override + public Connection getConnection() { + return connection; + } +} diff --git a/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_interactionUpdatesMixin.java b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_interactionUpdatesMixin.java new file mode 100644 index 0000000..aebeb80 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_interactionUpdatesMixin.java @@ -0,0 +1,81 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; +import net.minecraft.network.protocol.game.ServerboundUseItemPacket; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +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(ServerGamePacketListenerImpl.class) +public class ServerGamePacketListenerImpl_interactionUpdatesMixin +{ + @Inject(method = "handleUseItemOn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;useItemOn(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;", + shift = At.Shift.BEFORE + )) + private void beforeBlockInteracted(ServerboundUseItemOnPacket playerInteractBlockC2SPacket_1, CallbackInfo ci) + { + if (!CarpetSettings.interactionUpdates) + CarpetSettings.impendingFillSkipUpdates.set(true); + } + + @Inject(method = "handleUseItemOn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;useItemOn(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;", + shift = At.Shift.AFTER + )) + private void afterBlockInteracted(ServerboundUseItemOnPacket playerInteractBlockC2SPacket_1, CallbackInfo ci) + { + if (!CarpetSettings.interactionUpdates) + CarpetSettings.impendingFillSkipUpdates.set(false); + } + + @Inject(method = "handleUseItem", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;useItem(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/InteractionResult;", + shift = At.Shift.BEFORE + )) + private void beforeItemInteracted(ServerboundUseItemPacket packet, CallbackInfo ci) + { + if (!CarpetSettings.interactionUpdates) + CarpetSettings.impendingFillSkipUpdates.set(true); + } + + @Inject(method = "handleUseItem", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;useItem(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/InteractionResult;", + shift = At.Shift.AFTER + )) + private void afterItemInteracted(ServerboundUseItemPacket packet, CallbackInfo ci) + { + if (!CarpetSettings.interactionUpdates) + CarpetSettings.impendingFillSkipUpdates.set(false); + } + @Inject(method = "handlePlayerAction", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;handleBlockBreakAction(Lnet/minecraft/core/BlockPos;Lnet/minecraft/network/protocol/game/ServerboundPlayerActionPacket$Action;Lnet/minecraft/core/Direction;II)V", + shift = At.Shift.BEFORE + )) + private void beforeBlockBroken(ServerboundPlayerActionPacket packet, CallbackInfo ci) + { + if (!CarpetSettings.interactionUpdates) + CarpetSettings.impendingFillSkipUpdates.set(true); + } + + @Inject(method = "handlePlayerAction", at = @At( + value = "INVOKE", + target ="Lnet/minecraft/server/level/ServerPlayerGameMode;handleBlockBreakAction(Lnet/minecraft/core/BlockPos;Lnet/minecraft/network/protocol/game/ServerboundPlayerActionPacket$Action;Lnet/minecraft/core/Direction;II)V", + shift = At.Shift.AFTER + )) + private void afterBlockBroken(ServerboundPlayerActionPacket packet, CallbackInfo ci) + { + if (!CarpetSettings.interactionUpdates) + CarpetSettings.impendingFillSkipUpdates.set(false); + } + +} diff --git a/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_scarpetEventsMixin.java b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_scarpetEventsMixin.java new file mode 100644 index 0000000..5501163 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerGamePacketListenerImpl_scarpetEventsMixin.java @@ -0,0 +1,284 @@ +package carpet.mixins; + +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket; +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 static carpet.script.CarpetEventServer.Event.PLAYER_CLICKS_BLOCK; +import static carpet.script.CarpetEventServer.Event.PLAYER_DEPLOYS_ELYTRA; +import static carpet.script.CarpetEventServer.Event.PLAYER_DROPS_ITEM; +import static carpet.script.CarpetEventServer.Event.PLAYER_DROPS_STACK; +import static carpet.script.CarpetEventServer.Event.PLAYER_ESCAPES_SLEEP; +import static carpet.script.CarpetEventServer.Event.PLAYER_RELEASED_ITEM; +import static carpet.script.CarpetEventServer.Event.PLAYER_RIDES; +import static carpet.script.CarpetEventServer.Event.PLAYER_JUMPS; +import static carpet.script.CarpetEventServer.Event.PLAYER_RIGHT_CLICKS_BLOCK; +import static carpet.script.CarpetEventServer.Event.PLAYER_CHOOSES_RECIPE; +import static carpet.script.CarpetEventServer.Event.PLAYER_STARTS_SNEAKING; +import static carpet.script.CarpetEventServer.Event.PLAYER_STARTS_SPRINTING; +import static carpet.script.CarpetEventServer.Event.PLAYER_STOPS_SNEAKING; +import static carpet.script.CarpetEventServer.Event.PLAYER_STOPS_SPRINTING; +import static carpet.script.CarpetEventServer.Event.PLAYER_SWAPS_HANDS; +import static carpet.script.CarpetEventServer.Event.PLAYER_SWINGS_HAND; +import static carpet.script.CarpetEventServer.Event.PLAYER_SWITCHES_SLOT; +import static carpet.script.CarpetEventServer.Event.PLAYER_COMMAND; +import static carpet.script.CarpetEventServer.Event.PLAYER_USES_ITEM; +import static carpet.script.CarpetEventServer.Event.PLAYER_WAKES_UP; + +import net.minecraft.network.protocol.game.ServerboundContainerButtonClickPacket; +import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; +import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerInputPacket; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; +import net.minecraft.network.protocol.game.ServerboundSwingPacket; +import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; +import net.minecraft.network.protocol.game.ServerboundUseItemPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; + + +@Mixin(ServerGamePacketListenerImpl.class) +public class ServerGamePacketListenerImpl_scarpetEventsMixin +{ + @Shadow public ServerPlayer player; + + @Inject(method = "handlePlayerInput", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;setPlayerInput(FFZZ)V")) + private void checkMoves(ServerboundPlayerInputPacket p, CallbackInfo ci) + { + if (PLAYER_RIDES.isNeeded() && (p.getXxa() != 0.0F || p.getZza() != 0.0F || p.isJumping() || p.isShiftKeyDown())) + { + PLAYER_RIDES.onMountControls(player, p.getXxa(), p.getZza(), p.isJumping(), p.isShiftKeyDown()); + } + } + + @Inject(method = "handlePlayerAction", cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;drop(Z)Z", // dropSelectedItem + ordinal = 0, + shift = At.Shift.BEFORE + )) + private void onQItem(ServerboundPlayerActionPacket playerActionC2SPacket_1, CallbackInfo ci) + { + if(PLAYER_DROPS_ITEM.onPlayerEvent(player)) { + ci.cancel(); + } + } + + @Inject(method = "handlePlayerAction", cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;getItemInHand(Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/item/ItemStack;", + ordinal = 0, + shift = At.Shift.BEFORE + )) + private void onHandSwap(ServerboundPlayerActionPacket playerActionC2SPacket_1, CallbackInfo ci) + { + if(PLAYER_SWAPS_HANDS.onPlayerEvent(player)) ci.cancel(); + } + + @Inject(method = "handlePlayerAction", cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;drop(Z)Z", // dropSelectedItem + ordinal = 1, + shift = At.Shift.BEFORE + )) + private void onCtrlQItem(ServerboundPlayerActionPacket playerActionC2SPacket_1, CallbackInfo ci) + { + if(PLAYER_DROPS_STACK.onPlayerEvent(player)) { + ci.cancel(); + } + } + + + @Inject(method = "handleMovePlayer", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;jumpFromGround()V" + )) + private void onJump(ServerboundMovePlayerPacket playerMoveC2SPacket_1, CallbackInfo ci) + { + PLAYER_JUMPS.onPlayerEvent(player); + } + + @Inject(method = "handlePlayerAction", cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;handleBlockBreakAction(Lnet/minecraft/core/BlockPos;Lnet/minecraft/network/protocol/game/ServerboundPlayerActionPacket$Action;Lnet/minecraft/core/Direction;II)V", + shift = At.Shift.BEFORE + )) + private void onClicked(ServerboundPlayerActionPacket packet, CallbackInfo ci) + { + if (packet.getAction() == ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK) + if(PLAYER_CLICKS_BLOCK.onBlockAction(player, packet.getPos(), packet.getDirection())) { + ci.cancel(); + } + } + + @Redirect(method = "handlePlayerAction", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;releaseUsingItem()V" + )) + private void onStopUsing(ServerPlayer serverPlayerEntity) + { + if (PLAYER_RELEASED_ITEM.isNeeded()) + { + InteractionHand hand = serverPlayerEntity.getUsedItemHand(); + ItemStack stack = serverPlayerEntity.getUseItem().copy(); + serverPlayerEntity.releaseUsingItem(); + PLAYER_RELEASED_ITEM.onItemAction(player, hand, stack); + } + else + { + serverPlayerEntity.releaseUsingItem(); + } + } + + @Inject(method = "handleUseItemOn", cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayerGameMode;useItemOn(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;" + )) + private void onBlockInteracted(ServerboundUseItemOnPacket playerInteractBlockC2SPacket_1, CallbackInfo ci) + { + if (PLAYER_RIGHT_CLICKS_BLOCK.isNeeded()) + { + InteractionHand hand = playerInteractBlockC2SPacket_1.getHand(); + BlockHitResult hitRes = playerInteractBlockC2SPacket_1.getHitResult(); + if(PLAYER_RIGHT_CLICKS_BLOCK.onBlockHit(player, hand, hitRes)) { + ci.cancel(); + } + } + } + + @Inject(method = "handleUseItem", cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;resetLastActionTime()V" + )) + private void onItemClicked(ServerboundUseItemPacket playerInteractItemC2SPacket_1, CallbackInfo ci) + { + if (PLAYER_USES_ITEM.isNeeded()) + { + InteractionHand hand = playerInteractItemC2SPacket_1.getHand(); + if(PLAYER_USES_ITEM.onItemAction(player, hand, player.getItemInHand(hand).copy())) { + ci.cancel(); + } + } + } + + @Inject(method = "handlePlayerCommand", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;setShiftKeyDown(Z)V", + ordinal = 0 + )) + private void onStartSneaking(ServerboundPlayerCommandPacket clientCommandC2SPacket_1, CallbackInfo ci) + { + PLAYER_STARTS_SNEAKING.onPlayerEvent(player); + } + + @Inject(method = "handlePlayerCommand", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;setShiftKeyDown(Z)V", + ordinal = 1 + )) + private void onStopSneaking(ServerboundPlayerCommandPacket clientCommandC2SPacket_1, CallbackInfo ci) + { + PLAYER_STOPS_SNEAKING.onPlayerEvent(player); + } + + @Inject(method = "handlePlayerCommand", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;setSprinting(Z)V", + ordinal = 0 + )) + private void onStartSprinting(ServerboundPlayerCommandPacket clientCommandC2SPacket_1, CallbackInfo ci) + { + PLAYER_STARTS_SPRINTING.onPlayerEvent(player); + } + + @Inject(method = "handlePlayerCommand", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;setSprinting(Z)V", + ordinal = 1 + )) + private void onStopSprinting(ServerboundPlayerCommandPacket clientCommandC2SPacket_1, CallbackInfo ci) + { + PLAYER_STOPS_SPRINTING.onPlayerEvent(player); + } + + @Inject(method = "handlePlayerCommand", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;isSleeping()Z", + shift = At.Shift.BEFORE + )) + private void onWakeUp(ServerboundPlayerCommandPacket clientCommandC2SPacket_1, CallbackInfo ci) + { + //weird one - doesn't seem to work, maybe MP + if (player.isSleeping()) + PLAYER_WAKES_UP.onPlayerEvent(player); + else + PLAYER_ESCAPES_SLEEP.onPlayerEvent(player); + + } + + @Inject(method = "handlePlayerCommand", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;tryToStartFallFlying()Z", + shift = At.Shift.BEFORE + )) + private void onElytraEngage(ServerboundPlayerCommandPacket clientCommandC2SPacket_1, CallbackInfo ci) + { + PLAYER_DEPLOYS_ELYTRA.onPlayerEvent(player); + } + + @Inject(method = "handleContainerButtonClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;resetLastActionTime()V")) + private void onItemBeingPickedFromInventory(ServerboundContainerButtonClickPacket packet, CallbackInfo ci) + { + // crafts not int the crafting window + //CarpetSettings.LOG.error("Player clicks button "+packet.getButtonId()); + } + @Inject(method = "handlePlaceRecipe", cancellable = true, at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;resetLastActionTime()V")) + private void onRecipeSelectedInRecipeManager(ServerboundPlaceRecipePacket packet, CallbackInfo ci) + { + if (PLAYER_CHOOSES_RECIPE.isNeeded()) + { + if(PLAYER_CHOOSES_RECIPE.onRecipeSelected(player, packet.getRecipe(), packet.isShiftDown())) ci.cancel(); + } + } + + @Inject(method = "handleSetCarriedItem", at = @At("HEAD")) + private void onUpdatedSelectedSLot(ServerboundSetCarriedItemPacket packet, CallbackInfo ci) + { + if (PLAYER_SWITCHES_SLOT.isNeeded() && player.getServer() != null && player.getServer().isSameThread()) + { + PLAYER_SWITCHES_SLOT.onSlotSwitch(player, player.getInventory().selected, packet.getSlot()); + } + } + + @Inject(method = "handleAnimate", at = @At( + value = "INVOKE", target = + "Lnet/minecraft/server/level/ServerPlayer;resetLastActionTime()V", + shift = At.Shift.BEFORE) + ) + private void onSwing(ServerboundSwingPacket packet, CallbackInfo ci) + { + if (PLAYER_SWINGS_HAND.isNeeded() && !player.swinging) + { + PLAYER_SWINGS_HAND.onHandAction(player, packet.getHand()); + } + } + + @Inject(method = "handleChatCommand(Lnet/minecraft/network/protocol/game/ServerboundChatCommandPacket;)V", + at = @At(value = "HEAD") + ) + private void onChatCommandMessage(ServerboundChatCommandPacket serverboundChatCommandPacket, CallbackInfo ci) { + if (PLAYER_COMMAND.isNeeded()) + { + PLAYER_COMMAND.onPlayerMessage(player, serverboundChatCommandPacket.command()); + } + } +} diff --git a/src/main/java/carpet/mixins/ServerGamePacketListenerimpl_connectionMixin.java b/src/main/java/carpet/mixins/ServerGamePacketListenerimpl_connectionMixin.java new file mode 100644 index 0000000..1811168 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerGamePacketListenerimpl_connectionMixin.java @@ -0,0 +1,34 @@ +package carpet.mixins; + +import carpet.network.CarpetClient; +import carpet.network.ServerNetworkHandler; +import net.minecraft.network.protocol.PacketUtils; +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; +import net.minecraft.network.protocol.game.ServerGamePacketListener; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +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(ServerGamePacketListenerImpl.class) +public class ServerGamePacketListenerimpl_connectionMixin +{ + @Shadow + public ServerPlayer player; + + @Inject(method = "handleCustomPayload", at = @At("HEAD"), cancellable = true) + private void onCustomCarpetPayload(ServerboundCustomPayloadPacket serverboundCustomPayloadPacket, CallbackInfo ci) + { + if (serverboundCustomPayloadPacket.payload() instanceof CarpetClient.CarpetPayload cpp) { + // We should force onto the main thread here + // ServerNetworkHandler.handleData can possibly mutate data that isn't + // thread safe, and also allows for client commands to be executed + PacketUtils.ensureRunningOnSameThread(serverboundCustomPayloadPacket, (ServerGamePacketListener) this, player.serverLevel()); + ServerNetworkHandler.onClientData(player, cpp.data()); + ci.cancel(); + } + } +} diff --git a/src/main/java/carpet/mixins/ServerLevel_scarpetMixin.java b/src/main/java/carpet/mixins/ServerLevel_scarpetMixin.java new file mode 100644 index 0000000..b9b870b --- /dev/null +++ b/src/main/java/carpet/mixins/ServerLevel_scarpetMixin.java @@ -0,0 +1,110 @@ +package carpet.mixins; + +import carpet.fakes.ServerWorldInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.entity.LevelEntityGetter; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.WritableLevelData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.function.Supplier; + +import static carpet.script.CarpetEventServer.Event.EXPLOSION; +import static carpet.script.CarpetEventServer.Event.LIGHTNING; +import static carpet.script.CarpetEventServer.Event.CHUNK_UNLOADED; + +@Mixin(ServerLevel.class) +public abstract class ServerLevel_scarpetMixin extends Level implements ServerWorldInterface +{ + protected ServerLevel_scarpetMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i) + { + super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); + } + + @Inject(method = "tickChunk", locals = LocalCapture.CAPTURE_FAILHARD, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;addFreshEntity(Lnet/minecraft/world/entity/Entity;)Z", + shift = At.Shift.BEFORE, + ordinal = 1 + )) + private void onNaturalLightinig(LevelChunk chunk, int randomTickSpeed, CallbackInfo ci, + //ChunkPos chunkPos, boolean bl, int i, int j, Profiler profiler, BlockPos blockPos, boolean bl2) + ChunkPos chunkPos, boolean bl, int i, int j, ProfilerFiller profiler, BlockPos blockPos, DifficultyInstance localDifficulty, boolean bl2, LightningBolt lightningEntity) + { + if (LIGHTNING.isNeeded()) LIGHTNING.onWorldEventFlag((ServerLevel) (Object)this, blockPos, bl2?1:0); + } + + private Explosion.BlockInteraction getCMDestroyType(final GameRules.Key rule) { + return getGameRules().getBoolean(rule) ? Explosion.BlockInteraction.DESTROY_WITH_DECAY : Explosion.BlockInteraction.DESTROY; + } + + @Inject(method = "explode", at = @At("HEAD")) + private void handleExplosion(/*@Nullable*/ Entity entity, /*@Nullable*/ DamageSource damageSource, /*@Nullable*/ ExplosionDamageCalculator explosionBehavior, double d, double e, double f, float g, boolean bl, ExplosionInteraction explosionInteraction, ParticleOptions particleOptions, ParticleOptions particleOptions2, Holder soundEvent, CallbackInfoReturnable cir) + { + if (EXPLOSION.isNeeded()) { + Explosion.BlockInteraction var10000 = switch (explosionInteraction) { + case NONE -> Explosion.BlockInteraction.KEEP; + case BLOCK -> this.getCMDestroyType(GameRules.RULE_BLOCK_EXPLOSION_DROP_DECAY); + case MOB -> this.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING) ? this.getCMDestroyType(GameRules.RULE_MOB_EXPLOSION_DROP_DECAY) : Explosion.BlockInteraction.KEEP; + case TNT -> this.getCMDestroyType(GameRules.RULE_TNT_EXPLOSION_DROP_DECAY); + default -> throw new IncompatibleClassChangeError(); + }; + + EXPLOSION.onExplosion((ServerLevel) (Object) this, entity, null, d, e, f, g, bl, null, null, var10000); + } + } + + @Inject(method = "unload", at = @At("HEAD")) + private void handleChunkUnload(LevelChunk levelChunk, CallbackInfo ci) + { + if (CHUNK_UNLOADED.isNeeded()) + { + ServerLevel level = (ServerLevel)((Object)this); + CHUNK_UNLOADED.onChunkEvent(level, levelChunk.getPos(), false); + } + } + + @Final + @Shadow + private ServerLevelData serverLevelData; + @Shadow @Final private PersistentEntitySectionManager entityManager; + + @Unique + @Override + public ServerLevelData getWorldPropertiesCM(){ + return serverLevelData; + } + + @Unique + @Override + public LevelEntityGetter getEntityLookupCMPublic() { + return entityManager.getEntityGetter(); + } +} diff --git a/src/main/java/carpet/mixins/ServerLevel_tickMixin.java b/src/main/java/carpet/mixins/ServerLevel_tickMixin.java new file mode 100644 index 0000000..38a19e6 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerLevel_tickMixin.java @@ -0,0 +1,146 @@ +package carpet.mixins; + +import carpet.fakes.LevelInterface; +import carpet.utils.CarpetProfiler; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +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; + +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.storage.WritableLevelData; + +@Mixin(ServerLevel.class) +public abstract class ServerLevel_tickMixin extends Level implements LevelInterface +{ + protected ServerLevel_tickMixin(final WritableLevelData writableLevelData, final ResourceKey resourceKey, final RegistryAccess registryAccess, final Holder holder, final Supplier supplier, final boolean bl, final boolean bl2, final long l, final int i) + { + super(writableLevelData, resourceKey, registryAccess, holder, supplier, bl, bl2, l, i); + } + + private CarpetProfiler.ProfilerToken currentSection; + + @Inject(method = "tick", at = @At( + value = "CONSTANT", + args = "stringValue=weather" + )) + private void startWeatherSection(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section((Level)(Object)this, "Environment", CarpetProfiler.TYPE.GENERAL); + } + @Inject(method = "tick", at = @At( + value = "CONSTANT", + args = "stringValue=tickPending" + )) + private void stopWeatherStartTileTicks(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Schedule Ticks", CarpetProfiler.TYPE.GENERAL); + } + } + @Inject(method = "tick", at = @At( + value = "CONSTANT", + args = "stringValue=raid" + )) + private void stopTileTicksStartRaid(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Raid", CarpetProfiler.TYPE.GENERAL); + } + } + + @Inject(method = "tick", at = @At( + value = "CONSTANT", + args = "stringValue=chunkSource" + )) + private void stopRaid(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + } + } + @Inject(method = "tick", at = @At( + value = "CONSTANT", + args = "stringValue=blockEvents" + )) + private void startBlockEvents(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Block Events", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tick", at = @At( + value = "CONSTANT", + args = "stringValue=entities" + )) + private void stopBlockEventsStartEntitySection(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + if (currentSection != null) + { + CarpetProfiler.end_current_section(currentSection); + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Entities", CarpetProfiler.TYPE.GENERAL); + } + } + + @Inject(method = "tick", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;tickBlockEntities()V", + shift = At.Shift.BEFORE + )) + private void endEntitySection(BooleanSupplier booleanSupplier_1, CallbackInfo ci) + { + CarpetProfiler.end_current_section(currentSection); + currentSection = null; + } + + // Chunk + + @Inject(method = "tickChunk", at = @At("HEAD")) + private void startThunderSpawningSection(CallbackInfo ci) { + // Counting it in spawning because it's spawning skeleton horses + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Spawning", CarpetProfiler.TYPE.GENERAL); + } + + @Inject(method = "tickChunk", at = @At( + value = "CONSTANT", + args = "stringValue=iceandsnow" + )) + private void endThunderSpawningAndStartIceSnowRandomTicks(CallbackInfo ci) { + if (currentSection != null) { + CarpetProfiler.end_current_section(currentSection); + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Environment", CarpetProfiler.TYPE.GENERAL); + } + } + + @Inject(method = "tickChunk", at = @At( + value = "CONSTANT", + args = "stringValue=tickBlocks" + )) + private void endIceAndSnowAndStartRandomTicks(CallbackInfo ci) { + if (currentSection != null) { + CarpetProfiler.end_current_section(currentSection); + currentSection = CarpetProfiler.start_section((Level) (Object) this, "Random Ticks", CarpetProfiler.TYPE.GENERAL); + } + } + + @Inject(method = "tickChunk", at = @At("RETURN")) + private void endRandomTicks(CallbackInfo ci) { + if (currentSection != null) { + CarpetProfiler.end_current_section(currentSection); + currentSection = null; + } + } + +} diff --git a/src/main/java/carpet/mixins/ServerPlayerGameMode_antiCheatMixin.java b/src/main/java/carpet/mixins/ServerPlayerGameMode_antiCheatMixin.java new file mode 100644 index 0000000..ea8dfa6 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerPlayerGameMode_antiCheatMixin.java @@ -0,0 +1,44 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = Player.class) +public abstract class ServerPlayerGameMode_antiCheatMixin extends LivingEntity +{ + @Shadow public abstract double entityInteractionRange(); + + @Shadow public abstract double blockInteractionRange(); + + protected ServerPlayerGameMode_antiCheatMixin(final EntityType entityType, final Level level) + { + super(entityType, level); + } + + @Inject(method = "canInteractWithBlock", at = @At("HEAD"), cancellable = true) + private void canInteractLongRangeBlock(BlockPos pos, double d, CallbackInfoReturnable cir) + { + double maxRange = blockInteractionRange() + d; + maxRange = maxRange * maxRange; + if (CarpetSettings.antiCheatDisabled && maxRange < 1024 && getEyePosition().distanceToSqr(Vec3.atCenterOf(pos)) < 1024) cir.setReturnValue(true); + } + + @Inject(method = "canInteractWithEntity(Lnet/minecraft/world/phys/AABB;D)Z", at = @At("HEAD"), cancellable = true) + private void canInteractLongRangeEntity(AABB aabb, double d, CallbackInfoReturnable cir) + { + double maxRange = entityInteractionRange() + d; + maxRange = maxRange * maxRange; + if (CarpetSettings.antiCheatDisabled && maxRange < 1024 && aabb.distanceToSqr(getEyePosition()) < 1024) cir.setReturnValue(true); + } +} diff --git a/src/main/java/carpet/mixins/ServerPlayerGameMode_cactusMixin.java b/src/main/java/carpet/mixins/ServerPlayerGameMode_cactusMixin.java new file mode 100644 index 0000000..d4cb576 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerPlayerGameMode_cactusMixin.java @@ -0,0 +1,32 @@ +package carpet.mixins; + +import carpet.helpers.BlockRotator; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ServerPlayerGameMode.class) +public class ServerPlayerGameMode_cactusMixin +{ + + @Redirect(method = "useItemOn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;useItemOn(Lnet/minecraft/world/item/ItemStack;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/ItemInteractionResult;" + )) + private ItemInteractionResult activateWithOptionalCactus(final BlockState blockState, final ItemStack itemStack, final Level world_1, final Player playerEntity_1, final InteractionHand hand_1, final BlockHitResult blockHitResult_1) + { + boolean flipped = BlockRotator.flipBlockWithCactus(blockState, world_1, playerEntity_1, hand_1, blockHitResult_1); + if (flipped) + return ItemInteractionResult.SUCCESS; + + return blockState.useItemOn(itemStack, world_1, playerEntity_1, hand_1, blockHitResult_1); + } +} diff --git a/src/main/java/carpet/mixins/ServerPlayerGameMode_scarpetEventsMixin.java b/src/main/java/carpet/mixins/ServerPlayerGameMode_scarpetEventsMixin.java new file mode 100644 index 0000000..ed3beb6 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerPlayerGameMode_scarpetEventsMixin.java @@ -0,0 +1,85 @@ +package carpet.mixins; + +import carpet.fakes.ServerPlayerInteractionManagerInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import static carpet.script.CarpetEventServer.Event.PLAYER_BREAK_BLOCK; +import static carpet.script.CarpetEventServer.Event.PLAYER_INTERACTS_WITH_BLOCK; + + +@Mixin(ServerPlayerGameMode.class) +public class ServerPlayerGameMode_scarpetEventsMixin implements ServerPlayerInteractionManagerInterface +{ + @Shadow public ServerPlayer player; + + @Shadow private boolean isDestroyingBlock; + + @Shadow private BlockPos destroyPos; + + @Shadow private int lastSentState; + + @Shadow public ServerLevel level; + + @Inject(method = "destroyBlock", locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true, at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;removeBlock(Lnet/minecraft/core/BlockPos;Z)Z", + shift = At.Shift.BEFORE + )) + private void onBlockBroken(final BlockPos blockPos, final CallbackInfoReturnable cir, final BlockEntity blockEntity, final Block block, final BlockState blockState) + { + if(PLAYER_BREAK_BLOCK.onBlockBroken(player, blockPos, blockState)) { + this.level.sendBlockUpdated(blockPos, blockState, blockState, 3); + cir.setReturnValue(false); + cir.cancel(); + } + } + + @Inject(method = "useItemOn", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/advancements/critereon/ItemUsedOnLocationTrigger;trigger(Lnet/minecraft/server/level/ServerPlayer;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/item/ItemStack;)V", + shift = At.Shift.BEFORE + )) + private void onBlockActivated(ServerPlayer serverPlayerEntity, Level world, ItemStack stack, InteractionHand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) + { + PLAYER_INTERACTS_WITH_BLOCK.onBlockHit(player, hand, hitResult); + } + + @Override + public BlockPos getCurrentBreakingBlock() + { + if (!isDestroyingBlock) return null; + return destroyPos; + } + + @Override + public int getCurrentBlockBreakingProgress() + { + if (!isDestroyingBlock) return -1; + return lastSentState; + } + + @Override + public void setBlockBreakingProgress(int progress) + { + lastSentState = Mth.clamp(progress, -1, 10); + level.destroyBlockProgress(-1*this.player.getId(), destroyPos, lastSentState); + } +} diff --git a/src/main/java/carpet/mixins/ServerPlayer_actionPackMixin.java b/src/main/java/carpet/mixins/ServerPlayer_actionPackMixin.java new file mode 100644 index 0000000..9f7a39f --- /dev/null +++ b/src/main/java/carpet/mixins/ServerPlayer_actionPackMixin.java @@ -0,0 +1,38 @@ +package carpet.mixins; + +import carpet.fakes.ServerPlayerInterface; +import carpet.helpers.EntityPlayerActionPack; +import com.mojang.authlib.GameProfile; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayer.class) +public abstract class ServerPlayer_actionPackMixin implements ServerPlayerInterface +{ + @Unique + public EntityPlayerActionPack actionPack; + @Override + public EntityPlayerActionPack getActionPack() + { + return actionPack; + } + + @Inject(method = "", at = @At(value = "RETURN")) + private void onServerPlayerEntityContructor(MinecraftServer minecraftServer, ServerLevel serverLevel, GameProfile gameProfile, ClientInformation cli, CallbackInfo ci) + { + this.actionPack = new EntityPlayerActionPack((ServerPlayer) (Object) this); + } + + @Inject(method = "tick", at = @At(value = "HEAD")) + private void onTick(CallbackInfo ci) + { + actionPack.onUpdate(); + } +} diff --git a/src/main/java/carpet/mixins/ServerPlayer_scarpetEventMixin.java b/src/main/java/carpet/mixins/ServerPlayer_scarpetEventMixin.java new file mode 100644 index 0000000..15531f1 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerPlayer_scarpetEventMixin.java @@ -0,0 +1,134 @@ +package carpet.mixins; + +import carpet.fakes.EntityInterface; +import carpet.fakes.ServerPlayerInterface; +import carpet.script.EntityEventsGroup; +import com.mojang.authlib.GameProfile; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.stats.Stat; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.portal.DimensionTransition; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +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; + +@Mixin(ServerPlayer.class) +public abstract class ServerPlayer_scarpetEventMixin extends Player implements ServerPlayerInterface +{ + // to denote if the player reference is valid + + @Unique + private boolean isInvalidReference = false; + + public ServerPlayer_scarpetEventMixin(Level level, BlockPos blockPos, float f, GameProfile gameProfile) { + super(level, blockPos, f, gameProfile); + } + + //@Shadow protected abstract void completeUsingItem(); + + @Shadow public boolean wonGame; + + @Redirect(method = "completeUsingItem", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;completeUsingItem()V" + )) + private void finishedUsingItem(Player playerEntity) + { + if (PLAYER_FINISHED_USING_ITEM.isNeeded()) + { + InteractionHand hand = getUsedItemHand(); + if(!PLAYER_FINISHED_USING_ITEM.onItemAction((ServerPlayer) (Object)this, hand, getUseItem())) { + // do vanilla + super.completeUsingItem(); + } + } + else + { + // do vanilla + super.completeUsingItem(); + } + } + + @Inject(method = "awardStat", at = @At("HEAD")) + private void grabStat(Stat stat, int amount, CallbackInfo ci) + { + STATISTICS.onPlayerStatistic((ServerPlayer) (Object)this, stat, amount); + } + + @Inject(method = "die", at = @At("HEAD")) + private void onDeathEvent(DamageSource source, CallbackInfo ci) + { + ((EntityInterface)this).getEventContainer().onEvent(EntityEventsGroup.Event.ON_DEATH, source.getMsgId()); + if (PLAYER_DIES.isNeeded()) + { + PLAYER_DIES.onPlayerEvent((ServerPlayer) (Object)this); + } + } + + @Redirect(method = "setPlayerInput", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerPlayer;setShiftKeyDown(Z)V" + )) + private void setSneakingConditionally(ServerPlayer serverPlayerEntity, boolean sneaking) + { + if (!((EntityInterface)serverPlayerEntity.getVehicle()).isPermanentVehicle()) // won't since that method makes sure its not null + serverPlayerEntity.setShiftKeyDown(sneaking); + } + + private Vec3 previousLocation; + private ResourceKey previousDimension; + + @Inject(method = "changeDimension", at = @At("HEAD")) + private void logPreviousCoordinates(DimensionTransition serverWorld, CallbackInfoReturnable cir) + { + previousLocation = position(); + previousDimension = level().dimension(); //dimension type + } + + @Inject(method = "changeDimension", at = @At("RETURN")) + private void atChangeDimension(DimensionTransition destinationP, CallbackInfoReturnable cir) + { + if (PLAYER_CHANGES_DIMENSION.isNeeded()) + { + ServerPlayer player = (ServerPlayer) (Object)this; + DimensionTransition destinationTransition = destinationP; + ServerLevel destination = destinationTransition.newLevel(); + Vec3 to = null; + if (!wonGame || previousDimension != Level.END || destination.dimension() != Level.OVERWORLD) + { + to = position(); + } + PLAYER_CHANGES_DIMENSION.onDimensionChange(player, previousLocation, to, previousDimension, destination.dimension()); + } + }; + + @Override + public void invalidateEntityObjectReference() + { + isInvalidReference = true; + } + + @Override + public boolean isInvalidEntityObject() + { + return isInvalidReference; + } +} diff --git a/src/main/java/carpet/mixins/ServerStatus_motdMixin.java b/src/main/java/carpet/mixins/ServerStatus_motdMixin.java new file mode 100644 index 0000000..adae60a --- /dev/null +++ b/src/main/java/carpet/mixins/ServerStatus_motdMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.status.ServerStatus; +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(ServerStatus.class) +public class ServerStatus_motdMixin +{ + @Inject(method = "description", at = @At("HEAD"), cancellable = true) + private void getDescriptionAlternative(CallbackInfoReturnable cir) + { + if (!CarpetSettings.customMOTD.equals("_")) + { + cir.setReturnValue(Component.literal(CarpetSettings.customMOTD)); + cir.cancel(); + } + } +} diff --git a/src/main/java/carpet/mixins/ServerboundSetStructureBlockPacketMixin.java b/src/main/java/carpet/mixins/ServerboundSetStructureBlockPacketMixin.java new file mode 100644 index 0000000..fbb43e8 --- /dev/null +++ b/src/main/java/carpet/mixins/ServerboundSetStructureBlockPacketMixin.java @@ -0,0 +1,54 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ServerboundSetStructureBlockPacket; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.entity.StructureBlockEntity; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +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(ServerboundSetStructureBlockPacket.class) +public class ServerboundSetStructureBlockPacketMixin { + @Mutable @Final @Shadow + private BlockPos offset; + @Mutable @Final @Shadow + private Vec3i size; + + @Inject( + method = "(Lnet/minecraft/network/FriendlyByteBuf;)V", + at = @At("TAIL") + ) + private void structureBlockLimitsRead(FriendlyByteBuf buf, CallbackInfo ci) { + if (buf.readableBytes() == 6*4) { + // This will throw an exception if carpet is not installed on client + offset = new BlockPos(Mth.clamp(buf.readInt(), -CarpetSettings.structureBlockLimit, CarpetSettings.structureBlockLimit), Mth.clamp(buf.readInt(), -CarpetSettings.structureBlockLimit, CarpetSettings.structureBlockLimit), Mth.clamp(buf.readInt(), -CarpetSettings.structureBlockLimit, CarpetSettings.structureBlockLimit)); + size = new Vec3i(Mth.clamp(buf.readInt(), 0, CarpetSettings.structureBlockLimit), Mth.clamp(buf.readInt(), 0, CarpetSettings.structureBlockLimit), Mth.clamp(buf.readInt(), 0, CarpetSettings.structureBlockLimit)); + } + } + + @Inject( + method = "write", + at = @At("TAIL") + ) + private void structureBlockLimitsWrite(FriendlyByteBuf buf, CallbackInfo ci) { + //client method, only applicable if with carpet is on the server, or running locally + if (CarpetSettings.structureBlockLimit != StructureBlockEntity.MAX_SIZE_PER_AXIS) + { + buf.writeInt(this.offset.getX()); + buf.writeInt(this.offset.getY()); + buf.writeInt(this.offset.getZ()); + buf.writeInt(this.size.getX()); + buf.writeInt(this.size.getY()); + buf.writeInt(this.size.getZ()); + } + } +} diff --git a/src/main/java/carpet/mixins/SetBlockCommand_fillUpdatesMixin.java b/src/main/java/carpet/mixins/SetBlockCommand_fillUpdatesMixin.java new file mode 100644 index 0000000..95df92e --- /dev/null +++ b/src/main/java/carpet/mixins/SetBlockCommand_fillUpdatesMixin.java @@ -0,0 +1,23 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.commands.SetBlockCommand; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(SetBlockCommand.class) +public class SetBlockCommand_fillUpdatesMixin +{ + @Redirect(method = "setBlock", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;blockUpdated(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;)V" + )) + private static void conditionalUpdating(ServerLevel serverWorld, BlockPos blockPos_1, Block block_1) + { + if (CarpetSettings.fillUpdates) serverWorld.blockUpdated(blockPos_1, block_1); + } +} diff --git a/src/main/java/carpet/mixins/ShulkerBoxAccessMixin.java b/src/main/java/carpet/mixins/ShulkerBoxAccessMixin.java new file mode 100644 index 0000000..a0d59a4 --- /dev/null +++ b/src/main/java/carpet/mixins/ShulkerBoxAccessMixin.java @@ -0,0 +1,11 @@ +package carpet.mixins; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.client.model.ShulkerModel; + +@Mixin(net.minecraft.client.renderer.blockentity.ShulkerBoxRenderer.class) +public interface ShulkerBoxAccessMixin { + @Accessor("model") + ShulkerModel getModel(); +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/ShulkerBoxBlockEntity_creativeNoClipMixin.java b/src/main/java/carpet/mixins/ShulkerBoxBlockEntity_creativeNoClipMixin.java new file mode 100644 index 0000000..ec34448 --- /dev/null +++ b/src/main/java/carpet/mixins/ShulkerBoxBlockEntity_creativeNoClipMixin.java @@ -0,0 +1,25 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity; +import net.minecraft.world.level.material.PushReaction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ShulkerBoxBlockEntity.class) +public class ShulkerBoxBlockEntity_creativeNoClipMixin +{ + @Redirect(method = "moveCollidedEntities", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;getPistonPushReaction()Lnet/minecraft/world/level/material/PushReaction;" + )) + private PushReaction getPistonBehaviourOfNoClipPlayers(Entity entity) + { + if (CarpetSettings.creativeNoClip && entity instanceof Player && (((Player) entity).isCreative()) && ((Player) entity).getAbilities().flying) + return PushReaction.IGNORE; + return entity.getPistonPushReaction(); + } +} diff --git a/src/main/java/carpet/mixins/SpawnState_scarpetMixin.java b/src/main/java/carpet/mixins/SpawnState_scarpetMixin.java new file mode 100644 index 0000000..c670a4d --- /dev/null +++ b/src/main/java/carpet/mixins/SpawnState_scarpetMixin.java @@ -0,0 +1,20 @@ +package carpet.mixins; + +import carpet.fakes.SpawnHelperInnerInterface; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.PotentialCalculator; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(NaturalSpawner.SpawnState.class) +public class SpawnState_scarpetMixin implements SpawnHelperInnerInterface +{ + @Shadow @Final private PotentialCalculator spawnPotential; + + @Override + public PotentialCalculator getPotentialCalculator() + { + return spawnPotential; + } +} diff --git a/src/main/java/carpet/mixins/StandingAndWallBlockItem_creativeNoClipMixin.java b/src/main/java/carpet/mixins/StandingAndWallBlockItem_creativeNoClipMixin.java new file mode 100644 index 0000000..5d076ae --- /dev/null +++ b/src/main/java/carpet/mixins/StandingAndWallBlockItem_creativeNoClipMixin.java @@ -0,0 +1,38 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.StandingAndWallBlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(StandingAndWallBlockItem.class) +public class StandingAndWallBlockItem_creativeNoClipMixin +{ + @Redirect(method = "getPlacementState", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/LevelReader;isUnobstructed(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/phys/shapes/CollisionContext;)Z" + )) + private boolean canCreativePlayerPlace( + LevelReader worldView, BlockState state, BlockPos pos, CollisionContext context, + BlockPlaceContext itemcontext + ) + { + Player player = itemcontext.getPlayer(); + if (CarpetSettings.creativeNoClip && player != null && player.isCreative() && player.getAbilities().flying) + { + // copy from canPlace + VoxelShape voxelShape = state.getCollisionShape(worldView, pos, context); + return voxelShape.isEmpty() || worldView.isUnobstructed(player, voxelShape.move(pos.getX(), pos.getY(), pos.getZ())); + + } + return worldView.isUnobstructed(state, pos, context); + } +} diff --git a/src/main/java/carpet/mixins/StructureBlockEntity_fillUpdatesMixin.java b/src/main/java/carpet/mixins/StructureBlockEntity_fillUpdatesMixin.java new file mode 100644 index 0000000..6dd1558 --- /dev/null +++ b/src/main/java/carpet/mixins/StructureBlockEntity_fillUpdatesMixin.java @@ -0,0 +1,35 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.util.RandomSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.entity.StructureBlockEntity; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; + +@Mixin(StructureBlockEntity.class) +public abstract class StructureBlockEntity_fillUpdatesMixin +{ + @Redirect(method = "placeStructure(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate;)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate;placeInWorld(Lnet/minecraft/world/level/ServerLevelAccessor;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings;Lnet/minecraft/util/RandomSource;I)Z" + )) + private boolean onStructurePlacen(StructureTemplate structure, ServerLevelAccessor serverWorldAccess, BlockPos pos, BlockPos blockPos, StructurePlaceSettings placementData, RandomSource random, int i) + { + if(!CarpetSettings.fillUpdates) + CarpetSettings.impendingFillSkipUpdates.set(true); + try + { + return structure.placeInWorld(serverWorldAccess, pos, blockPos, placementData, random, i); + } + finally + { + CarpetSettings.impendingFillSkipUpdates.set(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/carpet/mixins/StructureBlockEntity_limitsMixin.java b/src/main/java/carpet/mixins/StructureBlockEntity_limitsMixin.java new file mode 100644 index 0000000..f0c758b --- /dev/null +++ b/src/main/java/carpet/mixins/StructureBlockEntity_limitsMixin.java @@ -0,0 +1,42 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.StructureBlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(StructureBlockEntity.class) +public abstract class StructureBlockEntity_limitsMixin +{ + @ModifyConstant( + method = "loadAdditional", + constant = @Constant(intValue = StructureBlockEntity.MAX_SIZE_PER_AXIS) + ) + private int positiveLimit(int original) { + return CarpetSettings.structureBlockLimit; + } + + @ModifyConstant( + method = "loadAdditional", + constant = @Constant(intValue = -StructureBlockEntity.MAX_SIZE_PER_AXIS) + ) + private int negativeLimit(int original) { + return -CarpetSettings.structureBlockLimit; + } + + @ModifyArg( + method = "saveStructure(Z)Z", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate;fillFromWorld(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/core/Vec3i;ZLnet/minecraft/world/level/block/Block;)V" + ), + index = 4 + ) + private Block ignoredBlock(Block original) { + return CarpetSettings.structureBlockIgnoredBlock; + } +} diff --git a/src/main/java/carpet/mixins/StructureBlockRenderer_mixin.java b/src/main/java/carpet/mixins/StructureBlockRenderer_mixin.java new file mode 100644 index 0000000..56bd797 --- /dev/null +++ b/src/main/java/carpet/mixins/StructureBlockRenderer_mixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.StructureBlockRenderer; +import net.minecraft.world.level.block.entity.StructureBlockEntity; +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(StructureBlockRenderer.class) +public abstract class StructureBlockRenderer_mixin implements BlockEntityRenderer +{ + @Inject(method = "getViewDistance", at = @At("HEAD"), cancellable = true) + void newLimit(CallbackInfoReturnable cir) + { + if (CarpetSettings.structureBlockOutlineDistance != 96) + cir.setReturnValue(CarpetSettings.structureBlockOutlineDistance); + } +} 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 0000000..9d33016 --- /dev/null +++ b/src/main/java/carpet/mixins/StructurePiece_scarpetPlopMixin.java @@ -0,0 +1,22 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +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 = "placeBlock", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/ChunkAccess;markPosForPostprocessing(Lnet/minecraft/core/BlockPos;)V" + )) + private void markOrNot(ChunkAccess chunk, BlockPos pos) + { + if (!CarpetSettings.skipGenerationChecks.get()) chunk.markPosForPostprocessing(pos); + } +} diff --git a/src/main/java/carpet/mixins/StructureTemplate_fillUpdatesMixin.java b/src/main/java/carpet/mixins/StructureTemplate_fillUpdatesMixin.java new file mode 100644 index 0000000..bd9cbb4 --- /dev/null +++ b/src/main/java/carpet/mixins/StructureTemplate_fillUpdatesMixin.java @@ -0,0 +1,34 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(StructureTemplate.class) +public class StructureTemplate_fillUpdatesMixin +{ + @Redirect( method = "placeInWorld", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/ServerLevelAccessor;blockUpdated(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;)V" + )) + private void skipUpdateNeighbours(ServerLevelAccessor serverWorldAccess, BlockPos pos, Block block) + { + if (!CarpetSettings.impendingFillSkipUpdates.get()) + serverWorldAccess.blockUpdated(pos, block); + } + + @Redirect(method = "placeInWorld", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings;getKnownShape()Z" + )) + private boolean skipPostprocess(StructurePlaceSettings structurePlacementData) + { + return structurePlacementData.getKnownShape() || CarpetSettings.impendingFillSkipUpdates.get(); + } +} diff --git a/src/main/java/carpet/mixins/SummonCommand_lightningMixin.java b/src/main/java/carpet/mixins/SummonCommand_lightningMixin.java new file mode 100644 index 0000000..97c4d45 --- /dev/null +++ b/src/main/java/carpet/mixins/SummonCommand_lightningMixin.java @@ -0,0 +1,44 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.commands.SummonCommand; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.animal.horse.SkeletonHorse; +import net.minecraft.world.level.GameRules; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(SummonCommand.class) +public class SummonCommand_lightningMixin +{ + @Redirect(method = "createEntity", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/Entity;blockPosition()Lnet/minecraft/core/BlockPos;" + )) + private static BlockPos addRiders(Entity entity) + { + // [CM] SummonNaturalLightning - if statement around + if (CarpetSettings.summonNaturalLightning && entity instanceof LightningBolt && !entity.getCommandSenderWorld().isClientSide) + { + ServerLevel world = (ServerLevel) entity.getCommandSenderWorld(); + BlockPos at = entity.blockPosition(); + DifficultyInstance localDifficulty_1 = world.getCurrentDifficultyAt(at); + boolean boolean_2 = world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && world.random.nextDouble() < (double)localDifficulty_1.getEffectiveDifficulty() * 0.01D; + if (boolean_2) { + SkeletonHorse skeletonHorseEntity_1 = EntityType.SKELETON_HORSE.create(world); + skeletonHorseEntity_1.setTrap(true); + skeletonHorseEntity_1.setAge(0); + skeletonHorseEntity_1.setPos(entity.getX(), entity.getY(), entity.getZ()); + world.addFreshEntity(skeletonHorseEntity_1); + } + } + return entity.blockPosition(); + } + +} diff --git a/src/main/java/carpet/mixins/SystemReport_addScarpetAppsMixin.java b/src/main/java/carpet/mixins/SystemReport_addScarpetAppsMixin.java new file mode 100644 index 0000000..b449059 --- /dev/null +++ b/src/main/java/carpet/mixins/SystemReport_addScarpetAppsMixin.java @@ -0,0 +1,26 @@ +package carpet.mixins; + +import java.util.function.Supplier; +import java.util.stream.Collectors; +import net.minecraft.SystemReport; +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; + +import carpet.CarpetServer; + +@Mixin(SystemReport.class) +public abstract class SystemReport_addScarpetAppsMixin +{ + @Shadow public abstract void setDetail(String name, Supplier valueSupplier); + + @Inject(method = "", at = @At("RETURN")) + private void fillSystemDetails(CallbackInfo info) { + if (CarpetServer.scriptServer == null || CarpetServer.scriptServer.modules.isEmpty()) return; + setDetail("Loaded Scarpet Apps", () -> { + return CarpetServer.scriptServer.modules.keySet().stream().collect(Collectors.joining("\n\t\t", "\n\t\t", "")); + }); + } +} diff --git a/src/main/java/carpet/mixins/TagPredicate_scarpetMixin.java b/src/main/java/carpet/mixins/TagPredicate_scarpetMixin.java new file mode 100644 index 0000000..63764d4 --- /dev/null +++ b/src/main/java/carpet/mixins/TagPredicate_scarpetMixin.java @@ -0,0 +1,54 @@ +package carpet.mixins; + +import carpet.fakes.BlockPredicateInterface; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import net.minecraft.core.HolderSet; +import net.minecraft.tags.TagKey; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.stream.Collectors; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +@Mixin(targets = "net.minecraft.commands.arguments.blocks.BlockPredicateArgument$TagPredicate") +public class TagPredicate_scarpetMixin implements BlockPredicateInterface +{ + @Shadow @Final private HolderSet tag; + + @Shadow @Final private Map vagueProperties; + + @Shadow @Final /*@Nullable*/ private CompoundTag nbt; + + @Override + public BlockState getCMBlockState() + { + return null; + } + + @Override + public TagKey getCMBlockTagKey() + { + // might be good to explose the holder set nature of it. + return tag.unwrap().left().orElse(null); + } + + @Override + public Map getCMProperties() + { + return vagueProperties.entrySet().stream().collect(Collectors.toMap( + e -> new StringValue(e.getKey()), + e -> new StringValue(e.getValue()) + )); + } + + @Override + public CompoundTag getCMDataTag() + { + return nbt; + } +} diff --git a/src/main/java/carpet/mixins/TheEndGatewayBlockEntity_creativeNoClipMixin.java b/src/main/java/carpet/mixins/TheEndGatewayBlockEntity_creativeNoClipMixin.java new file mode 100644 index 0000000..a1d9f69 --- /dev/null +++ b/src/main/java/carpet/mixins/TheEndGatewayBlockEntity_creativeNoClipMixin.java @@ -0,0 +1,15 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity; +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(TheEndGatewayBlockEntity.class) +public class TheEndGatewayBlockEntity_creativeNoClipMixin +{ + // removeme +} diff --git a/src/main/java/carpet/mixins/ThreadedLevelLightEngine_scarpetChunkCreationMixin.java b/src/main/java/carpet/mixins/ThreadedLevelLightEngine_scarpetChunkCreationMixin.java new file mode 100644 index 0000000..9e79275 --- /dev/null +++ b/src/main/java/carpet/mixins/ThreadedLevelLightEngine_scarpetChunkCreationMixin.java @@ -0,0 +1,98 @@ +package carpet.mixins; + +import java.util.concurrent.CompletableFuture; +import java.util.function.IntSupplier; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.server.level.ThreadedLevelLightEngine.TaskType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.gen.Invoker; + +import carpet.fakes.Lighting_scarpetChunkCreationInterface; +import carpet.fakes.ServerLightingProviderInterface; +import carpet.fakes.ThreadedAnvilChunkStorageInterface; + +@Mixin(ThreadedLevelLightEngine.class) +public abstract class ThreadedLevelLightEngine_scarpetChunkCreationMixin extends LevelLightEngine implements ServerLightingProviderInterface +{ + private ThreadedLevelLightEngine_scarpetChunkCreationMixin(final LightChunkGetter chunkProvider, final boolean hasBlockLight, final boolean hasSkyLight) + { + super(chunkProvider, hasBlockLight, hasSkyLight); + } + + @Shadow + protected abstract void addTask(final int x, final int z, final IntSupplier completedLevelSupplier, final TaskType stage, final Runnable task); + + @Shadow + @Final + private ChunkMap chunkMap; + + @Override + @Invoker("updateChunkStatus") + public abstract void invokeUpdateChunkStatus(ChunkPos pos); + + @Override + public void removeLightData(final ChunkAccess chunk) + { + ChunkPos pos = chunk.getPos(); + chunk.setLightCorrect(false); + + this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { + super.setLightEnabled(pos, false); + ((Lighting_scarpetChunkCreationInterface) this).removeLightData(SectionPos.getZeroNode(SectionPos.asLong(pos.x, 0, pos.z))); + }, + () -> "Remove light data " + pos + )); + } + + @Override + public CompletableFuture relight(ChunkAccess chunk) + { + ChunkPos pos = chunk.getPos(); + + this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.PRE_UPDATE, Util.name(() -> { + super.propagateLightSources(pos); + int minY = chunk.getMinBuildHeight(); + int maxY = chunk.getMaxBuildHeight(); + int minX = pos.getMinBlockX(); + int minZ = pos.getMinBlockZ(); + BlockPos.MutableBlockPos poss = new BlockPos.MutableBlockPos(); + for (int x = -1; x < 17; ++x) + { + for (int z = -1; z < 17; ++z) + { + if (x > 0 && x < 16 && z > 0 && z < 16) + {// not really efficient way to do it, but hey, we have bigger problems with this + continue; + } + for (int y = minY; y < maxY; ++y) + { + poss.set(x + minX, y, z + minZ); + super.checkBlock(poss); + } + } + } + }, + () -> "Relight chunk " + pos + )); + + return CompletableFuture.runAsync( + Util.name(() -> { + chunk.setLightCorrect(true); + //((ThreadedAnvilChunkStorageInterface) this.chunkMap).releaseRelightTicket(pos); + }, + () -> "Release relight ticket " + pos + ), + runnable -> this.addTask(pos.x, pos.z, () -> 0, ThreadedLevelLightEngine.TaskType.POST_UPDATE, runnable) + ); + } +} diff --git a/src/main/java/carpet/mixins/ThreadedLevelLightEngine_scarpetMixin.java b/src/main/java/carpet/mixins/ThreadedLevelLightEngine_scarpetMixin.java new file mode 100644 index 0000000..901654e --- /dev/null +++ b/src/main/java/carpet/mixins/ThreadedLevelLightEngine_scarpetMixin.java @@ -0,0 +1,61 @@ +package carpet.mixins; + +import carpet.fakes.ServerLightingProviderInterface; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.chunk.LightChunkGetter; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(ThreadedLevelLightEngine.class) +public abstract class ThreadedLevelLightEngine_scarpetMixin extends LevelLightEngine implements ServerLightingProviderInterface +{ + //@Shadow public abstract void checkBlock(BlockPos pos); + + //@Shadow public abstract void setLightEnabled(final ChunkPos chunkPos, final boolean bl); + + //@Shadow public abstract void propagateLightSources(final ChunkPos chunkPos); + + public ThreadedLevelLightEngine_scarpetMixin(LightChunkGetter chunkProvider, boolean hasBlockLight, boolean hasSkyLight) + { + super(chunkProvider, hasBlockLight, hasSkyLight); + } + + @Override + public void resetLight(ChunkAccess chunk, ChunkPos pos) + { + //super.setRetainData(pos, false); + //super.setLightEnabled(pos, false); + //for (int x = chpos.x-1; x <= chpos.x+1; x++ ) + // for (int z = chpos.z-1; z <= chpos.z+1; z++ ) + { + //ChunkPos pos = new ChunkPos(x, z); + int j; + for(j = -1; j < 17; ++j) { // skip some recomp + super.queueSectionData(LightLayer.BLOCK, SectionPos.of(pos, j), new DataLayer()); + super.queueSectionData(LightLayer.SKY, SectionPos.of(pos, j), new DataLayer()); + } + for(j = 0; j < 16; ++j) { + super.updateSectionStatus(SectionPos.of(pos, j), true); + } + + setLightEnabled(pos, true); + + propagateLightSources(pos); + // chunk.getLights().forEach((blockPos) -> { + // super.onBlockEmissionIncrease(blockPos, chunk.getLightEmission(blockPos)); + // }); + + } + + + + + } +} diff --git a/src/main/java/carpet/mixins/ThrowableProjectileMixin.java b/src/main/java/carpet/mixins/ThrowableProjectileMixin.java new file mode 100644 index 0000000..20533e2 --- /dev/null +++ b/src/main/java/carpet/mixins/ThrowableProjectileMixin.java @@ -0,0 +1,42 @@ +package carpet.mixins; + +import carpet.logging.LoggerRegistry; +import carpet.logging.logHelpers.TrajectoryLogHelper; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.projectile.ThrowableProjectile; +import net.minecraft.world.level.Level; +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(ThrowableProjectile.class) +public abstract class ThrowableProjectileMixin extends Entity +{ + private TrajectoryLogHelper logHelper; + public ThrowableProjectileMixin(EntityType entityType_1, Level world_1) { super(entityType_1, world_1); } + + @Inject(method = "(Lnet/minecraft/world/entity/EntityType;Lnet/minecraft/world/level/Level;)V", at = @At("RETURN")) + private void addLogger(EntityType entityType_1, Level world_1, CallbackInfo ci) + { + if (LoggerRegistry.__projectiles && !world_1.isClientSide) + logHelper = new TrajectoryLogHelper("projectiles"); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void tickCheck(CallbackInfo ci) + { + if (LoggerRegistry.__projectiles && logHelper != null) + logHelper.onTick(getX(), getY(), getZ(), getDeltaMovement()); + } + + @Override + public void remove(Entity.RemovalReason arg) + { + super.remove(arg); + if (LoggerRegistry.__projectiles && logHelper != null) + logHelper.onFinish(); + } +} diff --git a/src/main/java/carpet/mixins/TntBlock_noUpdateMixin.java b/src/main/java/carpet/mixins/TntBlock_noUpdateMixin.java new file mode 100644 index 0000000..691ae97 --- /dev/null +++ b/src/main/java/carpet/mixins/TntBlock_noUpdateMixin.java @@ -0,0 +1,21 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.TntBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(TntBlock.class) +public abstract class TntBlock_noUpdateMixin +{ + // Add carpet rule check for tntDoNotUpdate to an if statement. + @Redirect(method = "onPlace", at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;hasNeighborSignal(Lnet/minecraft/core/BlockPos;)Z")) + private boolean isTNTDoNotUpdate(Level world, BlockPos blockPos) + { + return !CarpetSettings.tntDoNotUpdate && world.hasNeighborSignal(blockPos); + } +} diff --git a/src/main/java/carpet/mixins/UseOnContext_cactusMixin.java b/src/main/java/carpet/mixins/UseOnContext_cactusMixin.java new file mode 100644 index 0000000..ded3117 --- /dev/null +++ b/src/main/java/carpet/mixins/UseOnContext_cactusMixin.java @@ -0,0 +1,27 @@ +package carpet.mixins; + +import carpet.helpers.BlockRotator; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.UseOnContext; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(UseOnContext.class) +public class UseOnContext_cactusMixin +{ + @Redirect(method = "getHorizontalDirection", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Player;getDirection()Lnet/minecraft/core/Direction;" + )) + private Direction getPlayerFacing(Player playerEntity) + { + Direction dir = playerEntity.getDirection(); + if (BlockRotator.flippinEligibility(playerEntity)) + { + dir = dir.getOpposite(); + } + return dir; + } +} diff --git a/src/main/java/carpet/mixins/Villager_aiMixin.java b/src/main/java/carpet/mixins/Villager_aiMixin.java new file mode 100644 index 0000000..359fbcf --- /dev/null +++ b/src/main/java/carpet/mixins/Villager_aiMixin.java @@ -0,0 +1,193 @@ +package carpet.mixins; + +import carpet.helpers.ParticleDisplay; +import carpet.utils.Messenger; +import carpet.utils.MobAI; +import net.minecraft.world.entity.ai.village.poi.PoiTypes; +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; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.memory.ExpirableValue; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.AbstractVillager; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.schedule.Activity; +import net.minecraft.world.item.BedItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.phys.Vec3; + +@Mixin(Villager.class) +public abstract class Villager_aiMixin extends AbstractVillager +{ + @Shadow protected abstract void setUnhappy(); + + @Shadow protected abstract int countFoodPointsInInventory(); + + @Shadow public abstract void eatAndDigestFood(); + + int totalFood; + boolean hasBed; + int displayAge; + + public Villager_aiMixin(EntityType entityType_1, Level world_1) + { + super(entityType_1, world_1); + } + + @Inject(method = "tick", at = @At("HEAD")) + private void ontick(CallbackInfo ci) + { + if (MobAI.isTracking(this, MobAI.TrackingType.IRON_GOLEM_SPAWNING)) + { + long time; + Optional> last_seen = this.brain.getMemories().get(MemoryModuleType.GOLEM_DETECTED_RECENTLY); + if (!last_seen.isPresent()) + { + time = 0; + } + else + { + time = last_seen.get().getTimeToLive(); + } + boolean recentlySeen = time > 0; + Optional optional_11 = this.brain.getMemory(MemoryModuleType.LAST_SLEPT); + //Optional optional_22 = this.brain.getOptionalMemory(MemoryModuleType.LAST_WORKED_AT_POI); + //boolean work = false; + boolean sleep = false; + boolean panic = this.brain.isActive(Activity.PANIC); + long currentTime = this.level().getGameTime(); + if (optional_11.isPresent()) { + sleep = (currentTime - optional_11.get()) < 24000L; + } + //if (optional_22.isPresent()) { + // work = (currentTime - optional_22.get().getTime()) < 36000L; + //} + + this.setCustomName(Messenger.c( + (sleep?"eb ":"fb ")+"\u263d ", + //(work?"eb ":"fb ")+"\u2692 ",//"\u26CF ", + (panic?"lb ":"fb ")+"\u2623 ",//"\u2622 \u2620 \u26A1 ", + (recentlySeen?"rb ":"lb ")+time )); + this.setCustomNameVisible(true); + } + else if (MobAI.isTracking(this, MobAI.TrackingType.BREEDING)) + { + if (tickCount % 50 == 0 || tickCount < 20) + { + totalFood = countFoodPointsInInventory() / 12; + hasBed = this.brain.getMemory(MemoryModuleType.HOME).isPresent(); + displayAge = getAge(); + + } + if (Math.abs(displayAge) < 100 && displayAge !=0) displayAge = getAge(); + + this.setCustomName(Messenger.c( + (hasBed?"eb ":"fb ")+"\u2616 ",//"\u263d ", + (totalFood>0?"eb ":"fb ")+"\u2668",(totalFood>0?"e ":"f ")+totalFood+" ", + (displayAge==0?"eb ":"fb ")+"\u2661",(displayAge==0?"e ":"f "+displayAge) + )); + this.setCustomNameVisible(true); + } + } + + @Inject(method = "mobInteract", at = @At("HEAD"), cancellable = true) + private void onInteract(Player playerEntity_1, InteractionHand hand_1, CallbackInfoReturnable cir) + { + if (MobAI.isTracking(this, MobAI.TrackingType.BREEDING)) + { + ItemStack itemStack_1 = playerEntity_1.getItemInHand(hand_1); + if (itemStack_1.getItem() == Items.EMERALD) + { + GlobalPos bedPos = this.brain.getMemory(MemoryModuleType.HOME).orElse(null); + if (bedPos == null || bedPos.dimension() != level().dimension()) // get Dimension + { + setUnhappy(); + ((ServerLevel) getCommandSenderWorld()).sendParticles(new BlockParticleOption(ParticleTypes.BLOCK_MARKER, Blocks.BARRIER.defaultBlockState()), getX(), getY() + getEyeHeight() + 1, getZ(), 1, 0.1, 0.1, 0.1, 0.0); + } + else + { + + ParticleDisplay.drawParticleLine((ServerPlayer) playerEntity_1, position(), Vec3.atCenterOf(bedPos.pos()), "dust 0 0 0 1", "happy_villager", 100, 0.2); // pos+0.5v + } + } + else if (itemStack_1.getItem() == Items.ROTTEN_FLESH) + { + while(countFoodPointsInInventory() >= 12) eatAndDigestFood(); + + } + else if (itemStack_1.getItem() instanceof BedItem) + { + List list_1 = ((ServerLevel) getCommandSenderWorld()).getPoiManager().getInRange( + type -> type.is(PoiTypes.HOME), + blockPosition(), + 48, PoiManager.Occupancy.ANY).toList(); + for (PoiRecord poi : list_1) + { + Vec3 pv = Vec3.atCenterOf(poi.getPos()); + if (!poi.hasSpace()) + { + ((ServerLevel) getCommandSenderWorld()).sendParticles(ParticleTypes.HAPPY_VILLAGER, + pv.x, pv.y+1.5, pv.z, + 50, 0.1, 0.3, 0.1, 0.0); + } + else if (canReachHome((Villager)(Object)this, poi.getPos(), poi)) + ((ServerLevel) getCommandSenderWorld()).sendParticles(ParticleTypes.END_ROD, + pv.x, pv.y+1, pv.z, + 50, 0.1, 0.3, 0.1, 0.0); + else + ((ServerLevel) getCommandSenderWorld()).sendParticles(new BlockParticleOption(ParticleTypes.BLOCK_MARKER, Blocks.BARRIER.defaultBlockState()), + pv.x, pv.y+1, pv.z, + 1, 0.1, 0.1, 0.1, 0.0); + } + } + cir.setReturnValue(InteractionResult.FAIL); + cir.cancel(); + } + } + + // stolen from VillagerMakeLove + private boolean canReachHome(Villager villager, BlockPos pos, PoiRecord poi) { + Path path = villager.getNavigation().createPath(pos, poi.getPoiType().value().validRange()); + return path != null && path.canReach(); + } + + + @Inject(method = "spawnGolemIfNeeded", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/phys/AABB;inflate(DDD)Lnet/minecraft/world/phys/AABB;", + shift = At.Shift.AFTER + )) + private void particleIt(ServerLevel serverWorld, long l, int i, CallbackInfo ci) + { + if (MobAI.isTracking(this, MobAI.TrackingType.IRON_GOLEM_SPAWNING)) + { + ((ServerLevel) getCommandSenderWorld()).sendParticles(new BlockParticleOption(ParticleTypes.BLOCK_MARKER, Blocks.BARRIER.defaultBlockState()), getX(), getY()+3, getZ(), 1, 0.1, 0.1, 0.1, 0.0); + } + } + + +} diff --git a/src/main/java/carpet/mixins/WitherBoss_moreBlueMixin.java b/src/main/java/carpet/mixins/WitherBoss_moreBlueMixin.java new file mode 100644 index 0000000..54c3994 --- /dev/null +++ b/src/main/java/carpet/mixins/WitherBoss_moreBlueMixin.java @@ -0,0 +1,25 @@ +package carpet.mixins; + + +import carpet.CarpetSettings; +import net.minecraft.util.RandomSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import net.minecraft.world.entity.boss.wither.WitherBoss; + +@Mixin(WitherBoss.class) +public class WitherBoss_moreBlueMixin +{ + @Redirect(method = "performRangedAttack(ILnet/minecraft/world/entity/LivingEntity;)V", at = @At( + value = "INVOKE", + target = "Lnet/minecraft/util/RandomSource;nextFloat()F") + ) + private float nextFloatAmplfied(RandomSource random) + { + if (CarpetSettings.moreBlueSkulls) return random.nextFloat()/100; + return random.nextFloat(); + } + +} diff --git a/src/main/java/carpet/mixins/WoolCarpetBlock_placeMixin.java b/src/main/java/carpet/mixins/WoolCarpetBlock_placeMixin.java new file mode 100644 index 0000000..e5a3239 --- /dev/null +++ b/src/main/java/carpet/mixins/WoolCarpetBlock_placeMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.utils.WoolTool; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.WoolCarpetBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(WoolCarpetBlock.class) // WoolCarpetBlock +public abstract class WoolCarpetBlock_placeMixin extends Block +{ + + public WoolCarpetBlock_placeMixin(Properties block$Settings_1) + { + super(block$Settings_1); + } + + public BlockState getStateForPlacement(BlockPlaceContext context) + { + BlockState state = super.getStateForPlacement(context); + if (context.getPlayer() != null && !context.getLevel().isClientSide) + { // getColor() + WoolTool.carpetPlacedAction(((WoolCarpetBlock)(Object)this).getColor(), context.getPlayer(), context.getClickedPos(), (ServerLevel) context.getLevel()); + } + return state; + } +} diff --git a/src/main/java/carpet/mixins/WorldBorder_syncedWorldBorderMixin.java b/src/main/java/carpet/mixins/WorldBorder_syncedWorldBorderMixin.java new file mode 100644 index 0000000..1ab8fcb --- /dev/null +++ b/src/main/java/carpet/mixins/WorldBorder_syncedWorldBorderMixin.java @@ -0,0 +1,25 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import carpet.patches.TickSyncedBorderExtent; +import net.minecraft.world.level.border.WorldBorder; +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(WorldBorder.class) +public class WorldBorder_syncedWorldBorderMixin +{ + @Shadow private WorldBorder.BorderExtent extent; + + @Inject(method = "lerpSizeBetween", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/border/WorldBorder;getListeners()Ljava/util/List;")) + private void getExtent(double d, double e, long l, CallbackInfo ci) + { + if (d != e && CarpetSettings.tickSyncedWorldBorders) + { + this.extent = new TickSyncedBorderExtent((WorldBorder) (Object) this, l, d, e); + } + } +} diff --git a/src/main/java/carpet/mixins/WorldGenRegion_scarpetPlopMixin.java b/src/main/java/carpet/mixins/WorldGenRegion_scarpetPlopMixin.java new file mode 100644 index 0000000..23bb45e --- /dev/null +++ b/src/main/java/carpet/mixins/WorldGenRegion_scarpetPlopMixin.java @@ -0,0 +1,19 @@ +package carpet.mixins; + +import carpet.CarpetSettings; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.WorldGenRegion; +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(WorldGenRegion.class) +public class WorldGenRegion_scarpetPlopMixin +{ + @Inject(method = "markPosForPostprocessing", at = @At("HEAD"), cancellable = true) + private void markOrNot(BlockPos blockPos, CallbackInfo ci) + { + if (CarpetSettings.skipGenerationChecks.get()) ci.cancel(); + } +} diff --git a/src/main/java/carpet/network/CarpetClient.java b/src/main/java/carpet/network/CarpetClient.java new file mode 100644 index 0000000..91622bd --- /dev/null +++ b/src/main/java/carpet/network/CarpetClient.java @@ -0,0 +1,113 @@ +package carpet.network; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.script.utils.ShapesRenderer; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; + +public class CarpetClient +{ + public record CarpetPayload(CompoundTag data) implements CustomPacketPayload + { + public static final StreamCodec STREAM_CODEC = CustomPacketPayload.codec(CarpetPayload::write, CarpetPayload::new); + + public static final Type TYPE = new CustomPacketPayload.Type<>(CARPET_CHANNEL); + + public CarpetPayload(FriendlyByteBuf input) + { + this(input.readNbt()); + } + + public void write(FriendlyByteBuf output) + { + output.writeNbt(data); + } + + @Override public Type type() + { + return TYPE; + } + } + + public static final String HI = "69"; + public static final String HELLO = "420"; + + public static ShapesRenderer shapes = null; + + private static LocalPlayer clientPlayer = null; + private static boolean isServerCarpet = false; + public static String serverCarpetVersion; + public static final ResourceLocation CARPET_CHANNEL = ResourceLocation.fromNamespaceAndPath("carpet", "hello"); + + public static void gameJoined(LocalPlayer player) + { + clientPlayer = player; + } + + public static void disconnect() + { + if (isServerCarpet) // multiplayer connection + { + isServerCarpet = false; + clientPlayer = null; + CarpetServer.onServerClosed(null); + CarpetServer.onServerDoneClosing(null); + } + else // singleplayer disconnect + { + CarpetServer.clientPreClosing(); + } + } + + public static void setCarpet() + { + isServerCarpet = true; + } + + public static LocalPlayer getPlayer() + { + return clientPlayer; + } + + public static boolean isCarpet() + { + return isServerCarpet; + } + + public static boolean sendClientCommand(String command) + { + if (!isServerCarpet && CarpetServer.minecraft_server == null) + { + return false; + } + ClientNetworkHandler.clientCommand(command); + return true; + } + + public static void onClientCommand(Tag t) + { + CarpetSettings.LOG.info("Server Response:"); + CompoundTag tag = (CompoundTag) t; + CarpetSettings.LOG.info(" - id: " + tag.getString("id")); + if (tag.contains("error")) + { + CarpetSettings.LOG.warn(" - error: " + tag.getString("error")); + } + if (tag.contains("output")) + { + ListTag outputTag = (ListTag) tag.get("output"); + for (int i = 0; i < outputTag.size(); i++) + { + CarpetSettings.LOG.info(" - response: " + Component.Serializer.fromJson(outputTag.getString(i), clientPlayer.registryAccess()).getString()); + } + } + } +} diff --git a/src/main/java/carpet/network/ClientNetworkHandler.java b/src/main/java/carpet/network/ClientNetworkHandler.java new file mode 100644 index 0000000..9e46370 --- /dev/null +++ b/src/main/java/carpet/network/ClientNetworkHandler.java @@ -0,0 +1,150 @@ +package carpet.network; + +import carpet.CarpetServer; +import carpet.CarpetExtension; +import carpet.CarpetSettings; +import carpet.api.settings.CarpetRule; +import carpet.api.settings.InvalidRuleValueException; +import carpet.api.settings.SettingsManager; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; + +public class ClientNetworkHandler +{ + private static final Map> dataHandlers = new HashMap>(); + + static + { + dataHandlers.put(CarpetClient.HI, (p, t) -> onHi(t.getAsString())); + dataHandlers.put("Rules", (p, t) -> { + CompoundTag ruleset = (CompoundTag) t; + for (String ruleKey : ruleset.getAllKeys()) + { + CompoundTag ruleNBT = (CompoundTag) ruleset.get(ruleKey); + SettingsManager manager = null; + String ruleName; + if (ruleNBT.contains("Manager")) + { + ruleName = ruleNBT.getString("Rule"); + String managerName = ruleNBT.getString("Manager"); + if (managerName.equals("carpet")) + { + manager = CarpetServer.settingsManager; + } + else + { + for (CarpetExtension extension : CarpetServer.extensions) + { + SettingsManager eManager = extension.extensionSettingsManager(); + if (eManager != null && managerName.equals(eManager.identifier())) + { + manager = eManager; + break; + } + } + } + } + else // Backwards compatibility + { + manager = CarpetServer.settingsManager; + ruleName = ruleKey; + } + CarpetRule rule = (manager != null) ? manager.getCarpetRule(ruleName) : null; + if (rule != null) + { + String value = ruleNBT.getString("Value"); + try + { + rule.set(null, value); + } + catch (InvalidRuleValueException ignored) + { + } + } + } + }); + dataHandlers.put("scShape", (p, t) -> { // deprecated // and unused // should remove for 1.17 + if (CarpetClient.shapes != null) + { + CarpetClient.shapes.addShape((CompoundTag) t); + } + }); + dataHandlers.put("scShapes", (p, t) -> { + if (CarpetClient.shapes != null) + { + CarpetClient.shapes.addShapes((ListTag) t); + } + }); + dataHandlers.put("clientCommand", (p, t) -> CarpetClient.onClientCommand(t)); + } + + // Ran on the Main Minecraft Thread + + private static void onHi(String version) + { + CarpetClient.setCarpet(); + CarpetClient.serverCarpetVersion = version; + 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); + } + // We can ensure that this packet is + // processed AFTER the player has joined + respondHello(); + } + + public static void respondHello() + { + CompoundTag data = new CompoundTag(); + data.putString(CarpetClient.HELLO, CarpetSettings.carpetVersion); + CarpetClient.getPlayer().connection.send(new ServerboundCustomPayloadPacket( + new CarpetClient.CarpetPayload(data) + )); + } + + public static void onServerData(CompoundTag compound, LocalPlayer player) + { + for (String key : compound.getAllKeys()) + { + if (dataHandlers.containsKey(key)) + { + try + { + dataHandlers.get(key).accept(player, compound.get(key)); + } + catch (Exception exc) + { + CarpetSettings.LOG.info("Corrupt carpet data for " + key); + } + } + else + { + CarpetSettings.LOG.error("Unknown carpet data: " + key); + } + } + } + + public static void clientCommand(String command) + { + CompoundTag tag = new CompoundTag(); + tag.putString("id", command); + tag.putString("command", command); + CompoundTag outer = new CompoundTag(); + outer.put("clientCommand", tag); + CarpetClient.getPlayer().connection.send(new ServerboundCustomPayloadPacket( + new CarpetClient.CarpetPayload(outer) + )); + } +} diff --git a/src/main/java/carpet/network/ServerNetworkHandler.java b/src/main/java/carpet/network/ServerNetworkHandler.java new file mode 100644 index 0000000..771bbaa --- /dev/null +++ b/src/main/java/carpet/network/ServerNetworkHandler.java @@ -0,0 +1,250 @@ +package carpet.network; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.api.settings.CarpetRule; +import carpet.api.settings.RuleHelper; +import carpet.fakes.ServerGamePacketListenerImplInterface; +import carpet.script.utils.SnoopyCommandSource; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +public class ServerNetworkHandler +{ + private static final Map remoteCarpetPlayers = new HashMap<>(); + private static final Set validCarpetPlayers = new HashSet<>(); + + private static final Map> dataHandlers = Map.of( + CarpetClient.HELLO, (p, t) -> onHello(p, t.getAsString()), + "clientCommand", (p, t) -> handleClientCommand(p, (CompoundTag) t) + ); + + public static void onPlayerJoin(ServerPlayer playerEntity) + { + if (!((ServerGamePacketListenerImplInterface) playerEntity.connection).getConnection().isMemoryConnection()) + { + CompoundTag data = new CompoundTag(); + data.putString(CarpetClient.HI, CarpetSettings.carpetVersion); + playerEntity.connection.send(new ClientboundCustomPayloadPacket(new CarpetClient.CarpetPayload(data))); + } + else + { + validCarpetPlayers.add(playerEntity); + } + } + + public static void onHello(ServerPlayer playerEntity, String version) + { + validCarpetPlayers.add(playerEntity); + remoteCarpetPlayers.put(playerEntity, version); + if (version.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: " + version); + } + DataBuilder data = DataBuilder.create(playerEntity.server); // tickrate related settings are sent on world change + CarpetServer.forEachManager(sm -> sm.getCarpetRules().forEach(data::withRule)); + playerEntity.connection.send(data.build()); + } + + public static void sendPlayerLevelData(ServerPlayer player, ServerLevel level) + { + if (CarpetSettings.superSecretSetting || !validCarpetPlayers.contains(player)) + { + //return; + } + // noop, used to send ticking information + //DataBuilder data = DataBuilder.create(player.server);//.withTickRate().withFrozenState().withTickPlayerActiveTimeout(); // .withSuperHotState() + //player.connection.send(data.build()); + } + + private static void handleClientCommand(ServerPlayer player, CompoundTag commandData) + { + String command = commandData.getString("command"); + String id = commandData.getString("id"); + List output = new ArrayList<>(); + Component[] error = {null}; + if (player.getServer() == null) + { + error[0] = Component.literal("No Server"); + } + else + { + player.getServer().getCommands().performPrefixedCommand( + new SnoopyCommandSource(player, error, output), command + ); + } + CompoundTag result = new CompoundTag(); + result.putString("id", id); + if (error[0] != null) + { + result.putString("error", error[0].getContents().toString()); + } + ListTag outputResult = new ListTag(); + for (Component line : output) + { + outputResult.add(StringTag.valueOf(Component.Serializer.toJson(line, player.registryAccess()))); + } + if (!output.isEmpty()) + { + result.put("output", outputResult); + } + player.connection.send(DataBuilder.create(player.server).withCustomNbt("clientCommand", result).build()); + // run command plug to command output, + } + + public static void onClientData(ServerPlayer player, CompoundTag compound) + { + for (String key : compound.getAllKeys()) + { + if (dataHandlers.containsKey(key)) + { + dataHandlers.get(key).accept(player, compound.get(key)); + } + else + { + CarpetSettings.LOG.warn("Unknown carpet client data: " + key); + } + } + } + + public static void updateRuleWithConnectedClients(CarpetRule rule) + { + if (CarpetSettings.superSecretSetting) + { + return; + } + for (ServerPlayer player : remoteCarpetPlayers.keySet()) + { + player.connection.send(DataBuilder.create(player.server).withRule(rule).build()); + } + } + + public static void broadcastCustomCommand(String command, Tag data) + { + if (CarpetSettings.superSecretSetting) + { + return; + } + for (ServerPlayer player : validCarpetPlayers) + { + player.connection.send(DataBuilder.create(player.server).withCustomNbt(command, data).build()); + } + } + + public static void sendCustomCommand(ServerPlayer player, String command, Tag data) + { + if (isValidCarpetPlayer(player)) + { + player.connection.send(DataBuilder.create(player.server).withCustomNbt(command, data).build()); + } + } + + public static void onPlayerLoggedOut(ServerPlayer player) + { + validCarpetPlayers.remove(player); + if (!((ServerGamePacketListenerImplInterface) player.connection).getConnection().isMemoryConnection()) + { + remoteCarpetPlayers.remove(player); + } + } + + public static void close() + { + remoteCarpetPlayers.clear(); + validCarpetPlayers.clear(); + } + + public static boolean isValidCarpetPlayer(ServerPlayer player) + { + if (CarpetSettings.superSecretSetting) + { + return false; + } + return validCarpetPlayers.contains(player); + + } + + public static String getPlayerStatus(ServerPlayer player) + { + if (remoteCarpetPlayers.containsKey(player)) + { + return "carpet " + remoteCarpetPlayers.get(player); + } + if (validCarpetPlayers.contains(player)) + { + return "carpet " + CarpetSettings.carpetVersion; + } + return "vanilla"; + } + + private static class DataBuilder + { + private CompoundTag tag; + // unused now, but hey + private MinecraftServer server; + + private static DataBuilder create(final MinecraftServer server) + { + return new DataBuilder(server); + } + + private DataBuilder(MinecraftServer server) + { + tag = new CompoundTag(); + this.server = server; + } + + private DataBuilder withRule(CarpetRule rule) + { + CompoundTag rules = (CompoundTag) tag.get("Rules"); + if (rules == null) + { + rules = new CompoundTag(); + tag.put("Rules", rules); + } + String identifier = rule.settingsManager().identifier(); + String key = rule.name(); + while (rules.contains(key)) + { + key = key + "2"; + } + CompoundTag ruleNBT = new CompoundTag(); + ruleNBT.putString("Value", RuleHelper.toRuleString(rule.value())); + ruleNBT.putString("Manager", identifier); + ruleNBT.putString("Rule", rule.name()); + rules.put(key, ruleNBT); + return this; + } + + public DataBuilder withCustomNbt(String key, Tag value) + { + tag.put(key, value); + return this; + } + + private ClientboundCustomPayloadPacket build() + { + return new ClientboundCustomPayloadPacket(new CarpetClient.CarpetPayload(tag)); + } + } +} diff --git a/src/main/java/carpet/patches/EntityPlayerMPFake.java b/src/main/java/carpet/patches/EntityPlayerMPFake.java new file mode 100644 index 0000000..3080387 --- /dev/null +++ b/src/main/java/carpet/patches/EntityPlayerMPFake.java @@ -0,0 +1,259 @@ +package carpet.patches; + +import carpet.CarpetSettings; +import com.mojang.authlib.GameProfile; +import net.minecraft.advancements.CriteriaTriggers; +import net.minecraft.core.BlockPos; +import net.minecraft.core.UUIDUtil; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.GameProfileCache; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.stats.Stats; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.portal.DimensionTransition; +import net.minecraft.world.phys.Vec3; +import carpet.fakes.ServerPlayerInterface; +import carpet.utils.Messenger; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@SuppressWarnings("EntityConstructor") +public class EntityPlayerMPFake extends ServerPlayer +{ + public Runnable fixStartingPosition = () -> {}; + public boolean isAShadow; + + // Returns true if it was successful, false if couldn't spawn due to the player not existing in Mojang servers + public static boolean createFake(String username, MinecraftServer server, Vec3 pos, double yaw, double pitch, ResourceKey dimensionId, GameType gamemode, boolean flying) + { + //prolly half of that crap is not necessary, but it works + ServerLevel worldIn = server.getLevel(dimensionId); + GameProfileCache.setUsesAuthentication(false); + GameProfile gameprofile; + try { + gameprofile = server.getProfileCache().get(username).orElse(null); //findByName .orElse(null) + } + finally { + GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication()); + } + if (gameprofile == null) + { + if (!CarpetSettings.allowSpawningOfflinePlayers) + { + return false; + } else { + gameprofile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); + } + } + GameProfile finalGP = gameprofile; + fetchGameProfile(gameprofile.getName()).thenAcceptAsync(p -> { + GameProfile current = finalGP; + if (p.isPresent()) + { + current = p.get(); + } + EntityPlayerMPFake instance = new EntityPlayerMPFake(server, worldIn, current, ClientInformation.createDefault(), false); + instance.fixStartingPosition = () -> instance.moveTo(pos.x, pos.y, pos.z, (float) yaw, (float) pitch); + server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), instance, new CommonListenerCookie(current, 0, instance.clientInformation(), false)); + instance.teleportTo(worldIn, pos.x, pos.y, pos.z, (float) yaw, (float) pitch); + instance.setHealth(20.0F); + instance.unsetRemoved(); + instance.getAttribute(Attributes.STEP_HEIGHT).setBaseValue(0.6F); + instance.gameMode.changeGameModeForPlayer(gamemode); + server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) (instance.yHeadRot * 256 / 360)), dimensionId);//instance.dimension); + server.getPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId);//instance.dimension); + //instance.world.getChunkManager(). updatePosition(instance); + instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 0x7f); // show all model layers (incl. capes) + instance.getAbilities().flying = flying; + }, server); + return true; + } + + private static CompletableFuture> fetchGameProfile(final String name) { + return SkullBlockEntity.fetchGameProfile(name); + } + + public static EntityPlayerMPFake createShadow(MinecraftServer server, ServerPlayer player) + { + player.getServer().getPlayerList().remove(player); + player.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login")); + ServerLevel worldIn = player.serverLevel();//.getWorld(player.dimension); + GameProfile gameprofile = player.getGameProfile(); + EntityPlayerMPFake playerShadow = new EntityPlayerMPFake(server, worldIn, gameprofile, player.clientInformation(), true); + playerShadow.setChatSession(player.getChatSession()); + server.getPlayerList().placeNewPlayer(new FakeClientConnection(PacketFlow.SERVERBOUND), playerShadow, new CommonListenerCookie(gameprofile, 0, player.clientInformation(), true)); + + playerShadow.setHealth(player.getHealth()); + playerShadow.connection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); + playerShadow.gameMode.changeGameModeForPlayer(player.gameMode.getGameModeForPlayer()); + ((ServerPlayerInterface) playerShadow).getActionPack().copyFrom(((ServerPlayerInterface) player).getActionPack()); + // this might create problems if a player logs back in... + playerShadow.getAttribute(Attributes.STEP_HEIGHT).setBaseValue(0.6F); + playerShadow.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, player.getEntityData().get(DATA_PLAYER_MODE_CUSTOMISATION)); + + + server.getPlayerList().broadcastAll(new ClientboundRotateHeadPacket(playerShadow, (byte) (player.yHeadRot * 256 / 360)), playerShadow.level().dimension()); + server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, playerShadow)); + //player.world.getChunkManager().updatePosition(playerShadow); + playerShadow.getAbilities().flying = player.getAbilities().flying; + return playerShadow; + } + + public static EntityPlayerMPFake respawnFake(MinecraftServer server, ServerLevel level, GameProfile profile, ClientInformation cli) + { + return new EntityPlayerMPFake(server, level, profile, cli, false); + } + + private EntityPlayerMPFake(MinecraftServer server, ServerLevel worldIn, GameProfile profile, ClientInformation cli, boolean shadow) + { + super(server, worldIn, profile, cli); + isAShadow = shadow; + } + + @Override + public void onEquipItem(final EquipmentSlot slot, final ItemStack previous, final ItemStack stack) + { + if (!isUsingItem()) super.onEquipItem(slot, previous, stack); + } + + @Override + public void kill() + { + kill(Messenger.s("Killed")); + } + + public void kill(Component reason) + { + shakeOff(); + + if (reason.getContents() instanceof TranslatableContents text && text.getKey().equals("multiplayer.disconnect.duplicate_login")) { + this.connection.onDisconnect(new DisconnectionDetails(reason)); + } else { + this.server.tell(new TickTask(this.server.getTickCount(), () -> { + this.connection.onDisconnect(new DisconnectionDetails(reason)); + })); + } + } + + @Override + public void tick() + { + if (this.getServer().getTickCount() % 10 == 0) + { + this.connection.resetPosition(); + this.serverLevel().getChunkSource().move(this); + } + try + { + super.tick(); + this.doTick(); + } + catch (NullPointerException ignored) + { + // happens with that paper port thingy - not sure what that would fix, but hey + // the game not gonna crash violently. + } + + + } + + private void shakeOff() + { + if (getVehicle() instanceof Player) stopRiding(); + for (Entity passenger : getIndirectPassengers()) + { + if (passenger instanceof Player) passenger.stopRiding(); + } + } + + @Override + public void die(DamageSource cause) + { + shakeOff(); + super.die(cause); + setHealth(20); + this.foodData = new FoodData(); + kill(this.getCombatTracker().getDeathMessage()); + } + + @Override + public String getIpAddress() + { + return "127.0.0.1"; + } + + @Override + public boolean allowsListing() { + return CarpetSettings.allowListingFakePlayers; + } + + @Override + protected void checkFallDamage(double y, boolean onGround, BlockState state, BlockPos pos) { + doCheckFallDamage(0.0, y, 0.0, onGround); + } + + @Override + public Entity changeDimension(DimensionTransition serverLevel) + { + super.changeDimension(serverLevel); + if (wonGame) { + ServerboundClientCommandPacket p = new ServerboundClientCommandPacket(ServerboundClientCommandPacket.Action.PERFORM_RESPAWN); + connection.handleClientCommand(p); + } + + // If above branch was taken, *this* has been removed and replaced, the new instance has been set + // on 'our' connection (which is now theirs, but we still have a ref). + if (connection.player.isChangingDimension()) { + connection.player.hasChangedDimension(); + } + return connection.player; + } + + @Override + public boolean hurt(DamageSource damageSource, float f) { + if (f > 0.0f && this.isDamageSourceBlocked(damageSource)) { + this.hurtCurrentlyUsedShield(f); + // equivalent of Player::blockUsingShield without wonky KB + if (damageSource.getDirectEntity() instanceof LivingEntity le && le.canDisableShield()) { + this.playSound(SoundEvents.SHIELD_BREAK, 0.8F, 0.8F + this.level().random.nextFloat() * 0.4F); + this.disableShield(); + } else { + // shield block sound probably + this.playSound(SoundEvents.SHIELD_BLOCK, 1.0F, 0.8F + this.level().random.nextFloat() * 0.4F); + } + // some stat tracking from LivingEntity::hurt + CriteriaTriggers.ENTITY_HURT_PLAYER.trigger((ServerPlayer)this, damageSource, f, 0, true); + if (f < 3.4028235E37F) { + ((ServerPlayer)this).awardStat(Stats.DAMAGE_BLOCKED_BY_SHIELD, Math.round(f * 10.0F)); + } + return false; + } + return super.hurt(damageSource, f); + } +} diff --git a/src/main/java/carpet/patches/FakeClientConnection.java b/src/main/java/carpet/patches/FakeClientConnection.java new file mode 100644 index 0000000..8a1a1b5 --- /dev/null +++ b/src/main/java/carpet/patches/FakeClientConnection.java @@ -0,0 +1,39 @@ +package carpet.patches; + +import carpet.fakes.ClientConnectionInterface; +import io.netty.channel.embedded.EmbeddedChannel; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.ProtocolInfo; +import net.minecraft.network.protocol.PacketFlow; + +public class FakeClientConnection extends Connection +{ + public FakeClientConnection(PacketFlow p) + { + super(p); + // compat with adventure-platform-fabric. This does NOT trigger other vanilla handlers for establishing a channel + // also makes #isOpen return true, allowing enderpearls to teleport fake players + ((ClientConnectionInterface)this).setChannel(new EmbeddedChannel()); + } + + @Override + public void setReadOnly() + { + } + + @Override + public void handleDisconnection() + { + } + + @Override + public void setListenerForServerboundHandshake(PacketListener packetListener) + { + } + + @Override + public void setupInboundProtocol(ProtocolInfo protocolInfo, T packetListener) + { + } +} \ No newline at end of file diff --git a/src/main/java/carpet/patches/NetHandlerPlayServerFake.java b/src/main/java/carpet/patches/NetHandlerPlayServerFake.java new file mode 100644 index 0000000..bfdd7f4 --- /dev/null +++ b/src/main/java/carpet/patches/NetHandlerPlayServerFake.java @@ -0,0 +1,48 @@ +package carpet.patches; + +import net.minecraft.network.Connection; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.entity.RelativeMovement; +import java.util.Set; + +public class NetHandlerPlayServerFake extends ServerGamePacketListenerImpl +{ + public NetHandlerPlayServerFake(final MinecraftServer minecraftServer, final Connection connection, final ServerPlayer serverPlayer, final CommonListenerCookie i) + { + super(minecraftServer, connection, serverPlayer, i); + } + + @Override + public void send(final Packet packetIn) + { + } + + @Override + public void disconnect(Component message) + { + if (message.getContents() instanceof TranslatableContents text && (text.getKey().equals("multiplayer.disconnect.idling") || text.getKey().equals("multiplayer.disconnect.duplicate_login"))) + { + ((EntityPlayerMPFake) player).kill(message); + } + } + + @Override + public void teleport(double d, double e, double f, float g, float h, Set set) + { + super.teleport(d, e, f, g, h, set); + if (player.serverLevel().getPlayerByUUID(player.getUUID()) != null) { + resetPosition(); + player.serverLevel().getChunkSource().move(player); + } + } + +} + + + diff --git a/src/main/java/carpet/patches/TickSyncedBorderExtent.java b/src/main/java/carpet/patches/TickSyncedBorderExtent.java new file mode 100644 index 0000000..fde797e --- /dev/null +++ b/src/main/java/carpet/patches/TickSyncedBorderExtent.java @@ -0,0 +1,173 @@ +package carpet.patches; + +import carpet.CarpetServer; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerTickRateManager; +import net.minecraft.util.Mth; +import net.minecraft.util.TimeUtil; +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.border.BorderStatus; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.phys.shapes.BooleanOp; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.NotNull; + +/** + * This class is essentially a copy of {@link net.minecraft.world.level.border.WorldBorder.MovingBorderExtent} + * but instead of using real time to lerp the border + * this class uses the in game ticks. + */ +@SuppressWarnings("JavadocReference") +public class TickSyncedBorderExtent implements WorldBorder.BorderExtent +{ + private final WorldBorder border; + private final long realDuration; + private final double tickDuration; + private final double from; + private final double to; + + private int ticks; + + public TickSyncedBorderExtent(WorldBorder border, long realDuration, double from, double to) + { + this.border = border; + this.realDuration = realDuration; + this.tickDuration = realDuration / 50.0; + this.from = from; + this.to = to; + this.ticks = 0; + } + + @Override + public double getMinX() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterX() - this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getMaxX() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterX() + this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getMinZ() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterZ() - this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getMaxZ() + { + int maxSize = this.border.getAbsoluteMaxSize(); + return Mth.clamp(this.border.getCenterZ() + this.getSize() / 2.0, -maxSize, maxSize); + } + + @Override + public double getSize() + { + double progress = this.ticks / this.tickDuration; + return progress < 1.0 ? Mth.lerp(progress, this.from, this.to) : this.to; + } + + @Override + public double getLerpSpeed() + { + return Math.abs(this.from - this.to) / this.realDuration; + } + + @Override + public long getLerpRemainingTime() + { + // Rough estimation + MinecraftServer server = CarpetServer.minecraft_server; + double ms; + if (server == null) + { + // can this even happen? + ms = 50.0; + } + else + { + ms = ((double)server.getAverageTickTimeNanos())/ TimeUtil.NANOSECONDS_PER_MILLISECOND; + ServerTickRateManager trm = server.tickRateManager(); + if (!trm.isSprinting()) + { + ms = Math.max(ms, trm.millisecondsPerTick()); + } + } + double tps = 1_000.0D / ms; + return (long) ((this.tickDuration - this.ticks) / tps * 1_000); + } + + @Override + public double getLerpTarget() + { + return this.to; + } + + @NotNull + @Override + public BorderStatus getStatus() + { + return this.to < this.from ? BorderStatus.SHRINKING : BorderStatus.GROWING; + } + + @Override + public void onAbsoluteMaxSizeChange() + { + + } + + @Override + public void onCenterChange() + { + + } + + @NotNull + @Override + public WorldBorder.BorderExtent update() + { + if (this.ticks++ % 20 == 0) + { + // We need to update any listeners + // Most importantly those that send updates to the client + // This is because the client logic uses real time + // So if the tick speed has changed we need to tell the client + for (BorderChangeListener listener : this.border.getListeners()) + { + // We do not want to update DelegateBorderChangeListener + // This updates borders in other dimensions + if (!(listener instanceof BorderChangeListener.DelegateBorderChangeListener)) + { + listener.onBorderSizeLerping(this.border, this.from, this.to, this.realDuration); + } + } + } + + return this.ticks >= this.tickDuration ? this.border.new StaticBorderExtent(this.to) : this; + } + + @NotNull + @Override + public VoxelShape getCollisionShape() + { + return Shapes.join( + Shapes.INFINITY, + Shapes.box( + Math.floor(this.getMinX()), + Double.NEGATIVE_INFINITY, + Math.floor(this.getMinZ()), + Math.ceil(this.getMaxX()), + Double.POSITIVE_INFINITY, + Math.ceil(this.getMaxZ()) + ), + BooleanOp.ONLY_FIRST + ); + } +} diff --git a/src/main/java/carpet/script/CarpetContext.java b/src/main/java/carpet/script/CarpetContext.java new file mode 100644 index 0000000..b428c92 --- /dev/null +++ b/src/main/java/carpet/script/CarpetContext.java @@ -0,0 +1,82 @@ +package carpet.script; + +import carpet.script.value.Value; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; + +public class CarpetContext extends Context +{ + /** + * @deprecated Use {@link #source()} or the new methods to access stuff in it instead + */ + @Deprecated(forRemoval = true) + public CommandSourceStack s; + private final BlockPos origin; + + public CarpetContext(CarpetScriptHost host, CommandSourceStack source) + { + this(host, source, BlockPos.ZERO); + } + + public CarpetContext(ScriptHost host, CommandSourceStack source, BlockPos origin) + { + super(host); + s = source; + this.origin = origin; + } + + @Override + public CarpetContext duplicate() + { + return new CarpetContext(this.host, this.s, this.origin); + } + + @Override + protected void initialize() + { + super.initialize(); + variables.put("_x", (c, t) -> Value.ZERO); + variables.put("_y", (c, t) -> Value.ZERO); + variables.put("_z", (c, t) -> Value.ZERO); + } + + public MinecraftServer server() + { + return s.getServer(); + } + + public ServerLevel level() + { + return s.getLevel(); + } + + public RegistryAccess registryAccess() + { + return s.getLevel().registryAccess(); + } + + public Registry registry(ResourceKey> resourceKey) + { + return registryAccess().registryOrThrow(resourceKey); + } + + public CommandSourceStack source() + { + return s; + } + + public BlockPos origin() + { + return origin; + } + + public void swapSource(CommandSourceStack source) + { + s = source; + } +} diff --git a/src/main/java/carpet/script/CarpetEventServer.java b/src/main/java/carpet/script/CarpetEventServer.java new file mode 100644 index 0000000..24de93a --- /dev/null +++ b/src/main/java/carpet/script/CarpetEventServer.java @@ -0,0 +1,1553 @@ +package carpet.script; + +import carpet.script.exception.IntegrityException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.InvalidCallbackException; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.utils.GlocalFlag; +import carpet.script.value.BlockValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.ListValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.stats.Stat; +import net.minecraft.stats.StatType; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.entity.npc.AbstractVillager; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.trading.Merchant; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +public class CarpetEventServer +{ + public final List scheduledCalls = new LinkedList<>(); + public final CarpetScriptServer scriptServer; + private static final List NOARGS = Collections.emptyList(); + public final Map customEvents = new HashMap<>(); + public GlocalFlag handleEvents = new GlocalFlag(true); + + public enum CallbackResult + { + SUCCESS, PASS, FAIL, CANCEL + } + + public static class Callback + { + public final String host; + @Nullable + public final String optionalTarget; + public final FunctionValue function; + public final List parametrizedArgs; + public final CarpetScriptServer scriptServer; + + public Callback(String host, @Nullable String target, FunctionValue function, List parametrizedArgs, CarpetScriptServer scriptServer) + { + this.host = host; + this.function = function; + this.optionalTarget = target; + this.parametrizedArgs = parametrizedArgs == null ? NOARGS : parametrizedArgs; + this.scriptServer = scriptServer; + } + + /** + * Used also in entity events + * + * @param sender - entity command source + * @param runtimeArgs = options + */ + public CallbackResult execute(CommandSourceStack sender, List runtimeArgs) + { + if (!this.parametrizedArgs.isEmpty()) + { + runtimeArgs = new ArrayList<>(runtimeArgs); + runtimeArgs.addAll(this.parametrizedArgs); + } + if (scriptServer.stopAll) + { + return CallbackResult.FAIL; // already stopped + } + return scriptServer.events.runEventCall( + sender.withPermission(Vanilla.MinecraftServer_getRunPermissionLevel(sender.getServer())), + host, optionalTarget, function, runtimeArgs); + } + + /** + * Used also in entity events + * + * @param sender - sender of the signal + * @param recipient - optional target player argument + * @param runtimeArgs = options + */ + public CallbackResult signal(CommandSourceStack sender, @Nullable ServerPlayer recipient, List runtimeArgs) + { + // recipent of the call doesn't match the handlingHost + return recipient != null && !recipient.getScoreboardName().equals(optionalTarget) ? CallbackResult.FAIL : execute(sender, runtimeArgs); + } + + + @Override + public String toString() + { + return function.getString() + ((host == null) ? "" : "(from " + host + (optionalTarget == null ? "" : "/" + optionalTarget) + ")"); + } + + public record Signature(String function, String host, String target) + { + } + + public static Signature fromString(String str) + { + Pattern find = Pattern.compile("(\\w+)(?:\\(from (\\w+)(?:/(\\w+))?\\))?"); + Matcher matcher = find.matcher(str); + if (matcher.matches()) + { + return new Signature(matcher.group(1), matcher.group(2), matcher.group(3)); + } + return new Signature(str, null, null); + } + } + + public static class ScheduledCall extends Callback + { + + private final CarpetContext ctx; + public long dueTime; + + public ScheduledCall(CarpetContext context, FunctionValue function, List args, long dueTime) + { + // ignoring target as we will be always calling self + super(context.host.getName(), null, function, args, (CarpetScriptServer) context.scriptServer()); + this.ctx = context.duplicate(); + this.dueTime = dueTime; + } + + /** + * used in scheduled calls + */ + public void execute() + { + scriptServer.events.runScheduledCall(ctx.origin(), ctx.source(), host, (CarpetScriptHost) ctx.host, function, parametrizedArgs); + } + } + + public static class CallbackList + { + + private List callList; + private final List removedCalls; + private boolean inCall; + private boolean inSignal; + public final int reqArgs; + final boolean isSystem; + final boolean perPlayerDistribution; + + public CallbackList(int reqArgs, boolean isSystem, boolean isGlobalOnly) + { + this.callList = new ArrayList<>(); + this.removedCalls = new ArrayList<>(); + this.inCall = false; + this.inSignal = false; + this.reqArgs = reqArgs; + this.isSystem = isSystem; + perPlayerDistribution = isSystem && !isGlobalOnly; + } + + public List inspectCurrentCalls() + { + return new ArrayList<>(callList); + } + + private void removeCallsIf(Predicate when) + { + if (!inCall && !inSignal) + { + callList.removeIf(when); + return; + } + // we are ok with list growing in the meantime and parallel access, we are only scanning. + for (int i = 0; i < callList.size(); i++) + { + Callback call = callList.get(i); + if (when.test(call)) + { + removedCalls.add(call); + } + } + } + + /** + * Handles only built-in events from the events system + * + * @param argumentSupplier + * @param cmdSourceSupplier + * @return Whether this event call has been cancelled + */ + public boolean call(Supplier> argumentSupplier, Supplier cmdSourceSupplier) + { + if (callList.isEmpty()) + { + return false; + } + CommandSourceStack source; + try + { + source = cmdSourceSupplier.get(); + } + catch (NullPointerException noReference) // todo figure out what happens when closing. + { + return false; + } + CarpetScriptServer scriptServer = Vanilla.MinecraftServer_getScriptServer(source.getServer()); + if (scriptServer.stopAll) + { + return false; + } + Boolean isCancelled = scriptServer.events.handleEvents.runIfEnabled(() -> { + Runnable profilerToken = Carpet.startProfilerSection("Scarpet events"); + List argv = argumentSupplier.get(); // empty for onTickDone + String nameCheck = perPlayerDistribution ? source.getTextName() : null; + assert argv.size() == reqArgs; + boolean cancelled = false; + try + { + // we are ok with list growing in the meantime + // which might happen during inCall or inSignal + inCall = true; + for (int i = 0; i < callList.size(); i++) + { + Callback call = callList.get(i); + // supressing calls where target player hosts simply don't match + // handling global hosts with player targets is left to when the host is resolved (few calls deeper). + if (nameCheck != null && call.optionalTarget != null && !nameCheck.equals(call.optionalTarget)) + { + continue; + } + CallbackResult result = call.execute(source, argv); + if (result == CallbackResult.CANCEL) + { + cancelled = true; + break; + } + if (result == CallbackResult.FAIL) + { + removedCalls.add(call); + } + } + } + finally + { + inCall = false; + } + for (Callback call : removedCalls) + { + callList.remove(call); + } + removedCalls.clear(); + profilerToken.run(); + return cancelled; + }); + return isCancelled != null && isCancelled; + } + + public int signal(CommandSourceStack sender, @Nullable ServerPlayer recipient, List callArg) + { + if (callList.isEmpty()) + { + return 0; + } + int successes = 0; + try + { + inSignal = true; + for (int i = 0; i < callList.size(); i++) + { + // skipping tracking of fails, its explicit call + if (callList.get(i).signal(sender, recipient, callArg) == CallbackResult.SUCCESS) + { + successes++; + } + } + } + finally + { + inSignal = false; + } + return successes; + } + + public boolean addFromExternal(CommandSourceStack source, String hostName, String funName, Consumer hostOnEventHandler, CarpetScriptServer scriptServer) + { + ScriptHost host = scriptServer.getAppHostByName(hostName); + if (host == null) + { + // impossible call to add + Carpet.Messenger_message(source, "r Unknown app " + hostName); + return false; + } + hostOnEventHandler.accept(host); + FunctionValue udf = host.getFunction(funName); + if (udf == null || udf.getArguments().size() != reqArgs) + { + // call won't match arguments + Carpet.Messenger_message(source, "r Callback doesn't expect required number of arguments: " + reqArgs); + return false; + } + String target = null; + if (host.isPerUser()) + { + try + { + target = source.getPlayerOrException().getScoreboardName(); + } + catch (CommandSyntaxException e) + { + Carpet.Messenger_message(source, "r Cannot add event to a player scoped app from a command without a player context"); + return false; + } + } + //all clear + //remove duplicates + + removeEventCall(hostName, target, udf.getString()); + callList.add(new Callback(hostName, target, udf, null, scriptServer)); + return true; + } + + public boolean addEventCallInternal(ScriptHost host, FunctionValue function, List args) + { + if (function == null || (function.getArguments().size() - args.size()) != reqArgs) + { + return false; + } + //removing duplicates + removeEventCall(host.getName(), host.user, function.getString()); + callList.add(new Callback(host.getName(), host.user, function, args, (CarpetScriptServer) host.scriptServer())); + return true; + } + + public void removeEventCall(String hostName, String target, String funName) + { + removeCallsIf((c) -> c.function.getString().equals(funName) + && (Objects.equals(c.host, hostName)) + && (Objects.equals(c.optionalTarget, target)) + ); + } + + public void removeAllCalls(CarpetScriptHost host) + { + removeCallsIf((c) -> (Objects.equals(c.host, host.getName())) + && (Objects.equals(c.optionalTarget, host.user))); + } + + public void createChildEvents(CarpetScriptHost host) + { + List copyCalls = new ArrayList<>(); + callList.forEach((c) -> + { + if ((Objects.equals(c.host, host.getName())) // TODO fix me + && c.optionalTarget == null) + { + copyCalls.add(new Callback(c.host, host.user, c.function, c.parametrizedArgs, host.scriptServer())); + } + }); + callList.addAll(copyCalls); + } + + public void clearEverything() + { + // when some moron puts /reload in an event call. + if (inSignal || inCall) + { + callList = new ArrayList<>(); + } + callList.clear(); + } + + public void sortByPriority(CarpetScriptServer scriptServer) + { + callList.sort(Comparator.comparingDouble(c -> -scriptServer.getAppHostByName(c.host).eventPriority)); + } + } + + public static class Event + { + public static final Map byName = new HashMap<>(); + + public static List publicEvents(CarpetScriptServer server) + { + List events = byName.values().stream().filter(e -> e.isPublic).collect(Collectors.toList()); + if (server != null) + { + events.addAll(server.events.customEvents.values()); + } + return events; + } + + static + { + Carpet.initCarpetEvents(); + } + + public static final Event START = new Event("server_starts", 0, true) + { + @Override + public void onTick(MinecraftServer server) + { + handler.call(Collections::emptyList, server::createCommandSourceStack); + } + }; + + public static final Event SHUTDOWN = new Event("server_shuts_down", 0, true) + { + @Override + public void onTick(MinecraftServer server) + { + handler.call(Collections::emptyList,server::createCommandSourceStack); + } + }; + + public static final Event TICK = new Event("tick", 0, true) + { + @Override + public void onTick(MinecraftServer server) + { + handler.call(Collections::emptyList,server::createCommandSourceStack); + } + }; + public static final Event NETHER_TICK = new Event("tick_nether", 0, true) + { + @Override + public boolean deprecated() + { + return true; + } + + @Override + public void onTick(MinecraftServer server) + { + handler.call(Collections::emptyList, () -> + server.createCommandSourceStack(). + withLevel(server.getLevel(Level.NETHER)) + ); + } + }; + public static final Event ENDER_TICK = new Event("tick_ender", 0, true) + { + @Override + public boolean deprecated() + { + return true; + } + + @Override + public void onTick(MinecraftServer server) + { + handler.call(Collections::emptyList, () -> + server.createCommandSourceStack(). + withLevel(server.getLevel(Level.END)) + ); + } + }; + public static final Event CHUNK_GENERATED = new Event("chunk_generated", 2, true) + { + @Override + public void onChunkEvent(ServerLevel world, ChunkPos chPos, boolean generated) + { + handler.call( + () -> Arrays.asList(new NumericValue(chPos.x << 4), new NumericValue(chPos.z << 4)), + () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + }; + public static final Event CHUNK_LOADED = new Event("chunk_loaded", 2, true) + { + @Override + public void onChunkEvent(ServerLevel world, ChunkPos chPos, boolean generated) + { + handler.call( + () -> Arrays.asList(new NumericValue(chPos.x << 4), new NumericValue(chPos.z << 4)), + () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + }; + + public static final Event CHUNK_UNLOADED = new Event("chunk_unloaded", 2, true) + { + @Override + public void onChunkEvent(ServerLevel world, ChunkPos chPos, boolean generated) + { + handler.call( + () -> Arrays.asList(new NumericValue(chPos.x << 4), new NumericValue(chPos.z << 4)), + () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + }; + + public static final Event PLAYER_JUMPS = new Event("player_jumps", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_DEPLOYS_ELYTRA = new Event("player_deploys_elytra", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_WAKES_UP = new Event("player_wakes_up", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_ESCAPES_SLEEP = new Event("player_escapes_sleep", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_RIDES = new Event("player_rides", 5, false) + { + @Override + public void onMountControls(ServerPlayer player, float strafeSpeed, float forwardSpeed, boolean jumping, boolean sneaking) + { + handler.call(() -> Arrays.asList(new EntityValue(player), + new NumericValue(forwardSpeed), new NumericValue(strafeSpeed), BooleanValue.of(jumping), BooleanValue.of(sneaking) + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_USES_ITEM = new Event("player_uses_item", 3, false) + { + @Override + public boolean onItemAction(ServerPlayer player, InteractionHand enumhand, ItemStack itemstack) + { + return handler.call(() -> + Arrays.asList( + new EntityValue(player), + ValueConversions.of(itemstack, player.level().registryAccess()), + StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand") + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_CLICKS_BLOCK = new Event("player_clicks_block", 3, false) + { + @Override + public boolean onBlockAction(ServerPlayer player, BlockPos blockpos, Direction facing) + { + return handler.call(() -> + Arrays.asList( + new EntityValue(player), + new BlockValue(null, player.serverLevel(), blockpos), + StringValue.of(facing.getName()) + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_RIGHT_CLICKS_BLOCK = new Event("player_right_clicks_block", 6, false) + { + @Override + public boolean onBlockHit(ServerPlayer player, InteractionHand enumhand, BlockHitResult hitRes) + { + return handler.call(() -> + { + ItemStack itemstack = player.getItemInHand(enumhand); + BlockPos blockpos = hitRes.getBlockPos(); + Direction enumfacing = hitRes.getDirection(); + Vec3 vec3d = hitRes.getLocation().subtract(blockpos.getX(), blockpos.getY(), blockpos.getZ()); + return Arrays.asList( + new EntityValue(player), + ValueConversions.of(itemstack, player.level().registryAccess()), + StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand"), + new BlockValue(null, player.serverLevel(), blockpos), + StringValue.of(enumfacing.getName()), + ListValue.of( + new NumericValue(vec3d.x), + new NumericValue(vec3d.y), + new NumericValue(vec3d.z) + ) + ); + }, player::createCommandSourceStack); + } + }; + public static final Event PLAYER_INTERACTS_WITH_BLOCK = new Event("player_interacts_with_block", 5, false) + { + @Override + public boolean onBlockHit(ServerPlayer player, InteractionHand enumhand, BlockHitResult hitRes) + { + handler.call(() -> + { + BlockPos blockpos = hitRes.getBlockPos(); + Direction enumfacing = hitRes.getDirection(); + Vec3 vec3d = hitRes.getLocation().subtract(blockpos.getX(), blockpos.getY(), blockpos.getZ()); + return Arrays.asList( + new EntityValue(player), + StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand"), + new BlockValue(null, player.serverLevel(), blockpos), + StringValue.of(enumfacing.getName()), + ListValue.of( + new NumericValue(vec3d.x), + new NumericValue(vec3d.y), + new NumericValue(vec3d.z) + ) + ); + }, player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_PLACING_BLOCK = new Event("player_placing_block", 4, false) + { + @Override + public boolean onBlockPlaced(ServerPlayer player, BlockPos pos, InteractionHand enumhand, ItemStack itemstack) + { + return handler.call(() -> Arrays.asList( + new EntityValue(player), + ValueConversions.of(itemstack, player.level().registryAccess()), + StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand"), + new BlockValue(null, player.serverLevel(), pos) + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_PLACES_BLOCK = new Event("player_places_block", 4, false) + { + @Override + public boolean onBlockPlaced(ServerPlayer player, BlockPos pos, InteractionHand enumhand, ItemStack itemstack) + { + handler.call(() -> Arrays.asList( + new EntityValue(player), + ValueConversions.of(itemstack, player.level().registryAccess()), + StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand"), + new BlockValue(null, player.serverLevel(), pos) + ), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_BREAK_BLOCK = new Event("player_breaks_block", 2, false) + { + @Override + public boolean onBlockBroken(ServerPlayer player, BlockPos pos, BlockState previousBS) + { + return handler.call( + () -> Arrays.asList(new EntityValue(player), new BlockValue(previousBS, player.serverLevel(), pos)), + player::createCommandSourceStack + ); + } + }; + public static final Event PLAYER_INTERACTS_WITH_ENTITY = new Event("player_interacts_with_entity", 3, false) + { + @Override + public boolean onEntityHandAction(ServerPlayer player, Entity entity, InteractionHand enumhand) + { + return handler.call(() -> Arrays.asList( + new EntityValue(player), new EntityValue(entity), StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand") + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_TRADES = new Event("player_trades", 5, false) + { + @Override + public void onTrade(ServerPlayer player, Merchant merchant, MerchantOffer tradeOffer) + { + RegistryAccess regs = player.level().registryAccess(); + handler.call(() -> Arrays.asList( + new EntityValue(player), + merchant instanceof final AbstractVillager villager ? new EntityValue(villager) : Value.NULL, + ValueConversions.of(tradeOffer.getBaseCostA(), regs), + ValueConversions.of(tradeOffer.getCostB(), regs), + ValueConversions.of(tradeOffer.getResult(), regs) + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_PICKS_UP_ITEM = new Event("player_picks_up_item", 2, false) + { + @Override + public boolean onItemAction(ServerPlayer player, InteractionHand enumhand, ItemStack itemstack) + { + handler.call(() -> Arrays.asList(new EntityValue(player), ValueConversions.of(itemstack, player.level().registryAccess())), player::createCommandSourceStack); + return false; + } + }; + + public static final Event PLAYER_ATTACKS_ENTITY = new Event("player_attacks_entity", 2, false) + { + @Override + public boolean onEntityHandAction(ServerPlayer player, Entity entity, InteractionHand enumhand) + { + return handler.call(() -> Arrays.asList(new EntityValue(player), new EntityValue(entity)), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_STARTS_SNEAKING = new Event("player_starts_sneaking", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_STOPS_SNEAKING = new Event("player_stops_sneaking", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_STARTS_SPRINTING = new Event("player_starts_sprinting", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_STOPS_SPRINTING = new Event("player_stops_sprinting", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + + public static final Event PLAYER_RELEASED_ITEM = new Event("player_releases_item", 3, false) + { + @Override + public boolean onItemAction(ServerPlayer player, InteractionHand enumhand, ItemStack itemstack) + { + // this.getStackInHand(this.getActiveHand()), this.activeItemStack) + handler.call(() -> + Arrays.asList( + new EntityValue(player), + ValueConversions.of(itemstack, player.level().registryAccess()), + StringValue.of(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand") + ), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_FINISHED_USING_ITEM = new Event("player_finishes_using_item", 3, false) + { + @Override + public boolean onItemAction(ServerPlayer player, InteractionHand enumhand, ItemStack itemstack) + { + // this.getStackInHand(this.getActiveHand()), this.activeItemStack) + return handler.call(() -> + Arrays.asList( + new EntityValue(player), + ValueConversions.of(itemstack, player.level().registryAccess()), + new StringValue(enumhand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand") + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_DROPS_ITEM = new Event("player_drops_item", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + return handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_DROPS_STACK = new Event("player_drops_stack", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + return handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_CHOOSES_RECIPE = new Event("player_chooses_recipe", 3, false) + { + @Override + public boolean onRecipeSelected(ServerPlayer player, ResourceLocation recipe, boolean fullStack) + { + return handler.call(() -> + Arrays.asList( + new EntityValue(player), + NBTSerializableValue.nameFromRegistryId(recipe), + BooleanValue.of(fullStack) + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_SWITCHES_SLOT = new Event("player_switches_slot", 3, false) + { + @Override + public void onSlotSwitch(ServerPlayer player, int from, int to) + { + if (from == to) + { + return; // initial slot update + } + handler.call(() -> + Arrays.asList( + new EntityValue(player), + new NumericValue(from), + new NumericValue(to) + ), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_SWAPS_HANDS = new Event("player_swaps_hands", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + return handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_SWINGS_HAND = new Event("player_swings_hand", 2, false) + { + @Override + public void onHandAction(ServerPlayer player, InteractionHand hand) + { + handler.call(() -> Arrays.asList( + new EntityValue(player), + StringValue.of(hand == InteractionHand.MAIN_HAND ? "mainhand" : "offhand") + ) + , player::createCommandSourceStack); + } + }; + public static final Event PLAYER_TAKES_DAMAGE = new Event("player_takes_damage", 4, false) + { + @Override + public boolean onDamage(Entity target, float amount, DamageSource source) + { + return handler.call(() -> + Arrays.asList( + new EntityValue(target), + new NumericValue(amount), + StringValue.of(source.getMsgId()), + source.getEntity() == null ? Value.NULL : new EntityValue(source.getEntity()) + ), target::createCommandSourceStack); + } + }; + public static final Event PLAYER_DEALS_DAMAGE = new Event("player_deals_damage", 3, false) + { + @Override + public boolean onDamage(Entity target, float amount, DamageSource source) + { + return handler.call(() -> + Arrays.asList(new EntityValue(source.getEntity()), new NumericValue(amount), new EntityValue(target)), + () -> source.getEntity().createCommandSourceStack() + ); + } + }; + public static final Event PLAYER_COLLIDES_WITH_ENTITY = new Event("player_collides_with_entity", 2, false) + { + @Override + public boolean onEntityHandAction(ServerPlayer player, Entity entity, InteractionHand enumhand) + { + handler.call(() -> Arrays.asList(new EntityValue(player), new EntityValue(entity)), player::createCommandSourceStack); + return false; + } + }; + + public static final Event PLAYER_DIES = new Event("player_dies", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_RESPAWNS = new Event("player_respawns", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_CHANGES_DIMENSION = new Event("player_changes_dimension", 5, false) + { + @Override + public void onDimensionChange(ServerPlayer player, Vec3 from, Vec3 to, ResourceKey fromDim, ResourceKey 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 = NBTSerializableValue.nameFromRegistryId(fromDim.location()); + Value toDimStr = NBTSerializableValue.nameFromRegistryId(dimTo.location()); + + handler.call(() -> Arrays.asList(new EntityValue(player), fromValue, fromDimStr, toValue, toDimStr), player::createCommandSourceStack); + } + }; + public static final Event PLAYER_CONNECTS = new Event("player_connects", 1, false) + { + @Override + public boolean onPlayerEvent(ServerPlayer player) + { + handler.call(() -> Collections.singletonList(new EntityValue(player)), player::createCommandSourceStack); + return false; + } + }; + public static final Event PLAYER_DISCONNECTS = new Event("player_disconnects", 2, false) + { + @Override + public boolean onPlayerMessage(ServerPlayer player, String message) + { + handler.call(() -> Arrays.asList(new EntityValue(player), new StringValue(message)), player::createCommandSourceStack); + return false; + } + }; + + public static final Event PLAYER_MESSAGE = new Event("player_message", 2, false) + { + @Override + public boolean onPlayerMessage(ServerPlayer player, String message) + { + return handler.call(() -> Arrays.asList(new EntityValue(player), new StringValue(message)), player::createCommandSourceStack); + } + }; + + public static final Event PLAYER_COMMAND = new Event("player_command", 2, false) + { + @Override + public boolean onPlayerMessage(ServerPlayer player, String message) + { + return handler.call(() -> Arrays.asList(new EntityValue(player), new StringValue(message)), player::createCommandSourceStack); + } + }; + + public static final Event STATISTICS = new Event("statistic", 4, false) + { + private ResourceLocation getStatId(Stat stat) + { + return stat.getType().getRegistry().getKey(stat.getValue()); + } + + private final Set skippedStats = Set.of( + Stats.TIME_SINCE_DEATH, + Stats.TIME_SINCE_REST, + Stats.PLAY_TIME, + Stats.TOTAL_WORLD_TIME + ); + + @Override + public void onPlayerStatistic(ServerPlayer player, Stat stat, int amount) + { + ResourceLocation id = getStatId(stat); + if (skippedStats.contains(id)) + { + return; + } + Registry> registry = player.level().registryAccess().registryOrThrow(Registries.STAT_TYPE); + handler.call(() -> Arrays.asList( + new EntityValue(player), + NBTSerializableValue.nameFromRegistryId(registry.getKey(stat.getType())), + NBTSerializableValue.nameFromRegistryId(id), + new NumericValue(amount) + ), player::createCommandSourceStack); + } + }; + public static final Event LIGHTNING = new Event("lightning", 2, true) + { + @Override + public void onWorldEventFlag(ServerLevel world, BlockPos pos, int flag) + { + handler.call( + () -> Arrays.asList( + new BlockValue(null, world, pos), + flag > 0 ? Value.TRUE : Value.FALSE + ), () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + }; + + //copy of Explosion.getCausingEntity() #TRACK# + private static LivingEntity getExplosionCausingEntity(Entity entity) + { + if (entity == null) + { + return null; + } + else if (entity instanceof final PrimedTnt tnt) + { + return tnt.getOwner(); + } + else if (entity instanceof final LivingEntity le) + { + return le; + } + else if (entity instanceof final Projectile p) + { + Entity owner = p.getOwner(); + if (owner instanceof final LivingEntity le) + { + return le; + } + } + return null; + } + + public static final Event EXPLOSION_OUTCOME = new Event("explosion_outcome", 8, true) + { + @Override + public void onExplosion(ServerLevel world, Entity e, Supplier attacker, double x, double y, double z, float power, boolean createFire, List affectedBlocks, List affectedEntities, Explosion.BlockInteraction type) + { + handler.call( + () -> Arrays.asList( + ListValue.fromTriple(x, y, z), + NumericValue.of(power), + EntityValue.of(e), + EntityValue.of(attacker != null ? attacker.get() : Event.getExplosionCausingEntity(e)), + StringValue.of(type.name().toLowerCase(Locale.ROOT)), + BooleanValue.of(createFire), + ListValue.wrap(affectedBlocks.stream().filter(b -> !world.isEmptyBlock(b)).map( // da heck they send air blocks + b -> new BlockValue(world.getBlockState(b), world, b) + )), + ListValue.wrap(affectedEntities.stream().map(EntityValue::of)) + ), () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + }; + + + public static final Event EXPLOSION = new Event("explosion", 6, true) + { + @Override + public void onExplosion(ServerLevel world, Entity e, Supplier attacker, double x, double y, double z, float power, boolean createFire, List affectedBlocks, List affectedEntities, Explosion.BlockInteraction type) + { + handler.call( + () -> Arrays.asList( + ListValue.fromTriple(x, y, z), + NumericValue.of(power), + EntityValue.of(e), + EntityValue.of(attacker != null ? attacker.get() : Event.getExplosionCausingEntity(e)), + StringValue.of(type.name().toLowerCase(Locale.ROOT)), + BooleanValue.of(createFire) + ), () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + }; + + @Deprecated + public static String getEntityLoadEventName(EntityType et) + { + return "entity_loaded_" + ValueConversions.of(BuiltInRegistries.ENTITY_TYPE.getKey(et)).getString(); + } + + @Deprecated + public static final Map, Event> ENTITY_LOAD = BuiltInRegistries.ENTITY_TYPE + .stream() + .map(et -> Map.entry(et, new Event(getEntityLoadEventName(et), 1, true, false) + { + @Override + public void onEntityAction(Entity entity, boolean created) + { + handler.call( + () -> Collections.singletonList(new EntityValue(entity)), + () -> entity.getServer().createCommandSourceStack().withLevel((ServerLevel) entity.level()).withPermission(Vanilla.MinecraftServer_getRunPermissionLevel(entity.getServer())) + ); + } + })).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + + public static String getEntityHandlerEventName(EntityType et) + { + return "entity_handler_" + ValueConversions.of(BuiltInRegistries.ENTITY_TYPE.getKey(et)).getString(); + } + + public static final Map, Event> ENTITY_HANDLER = BuiltInRegistries.ENTITY_TYPE + .stream() + .map(et -> Map.entry(et, new Event(getEntityHandlerEventName(et), 2, true, false) + { + @Override + public void onEntityAction(Entity entity, boolean created) + { + handler.call( + () -> Arrays.asList(new EntityValue(entity), BooleanValue.of(created)), + () -> entity.getServer().createCommandSourceStack().withLevel((ServerLevel) entity.level()).withPermission(Vanilla.MinecraftServer_getRunPermissionLevel(entity.getServer())) + ); + } + })) + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); + + // on projectile thrown (arrow from bows, crossbows, tridents, snoballs, e-pearls + + public final String name; + + public final CallbackList handler; + public final boolean isPublic; // public events can be targetted with __on_ defs + + public Event(String name, int reqArgs, boolean isGlobalOnly) + { + this(name, reqArgs, isGlobalOnly, true); + } + + public Event(String name, int reqArgs, boolean isGlobalOnly, boolean isPublic) + { + this.name = name; + this.handler = new CallbackList(reqArgs, true, isGlobalOnly); + this.isPublic = isPublic; + byName.put(name, this); + } + + public static List getAllEvents(CarpetScriptServer server, Predicate predicate) + { + List eventList = new ArrayList<>(CarpetEventServer.Event.byName.values()); + eventList.addAll(server.events.customEvents.values()); + if (predicate == null) + { + return eventList; + } + return eventList.stream().filter(predicate).toList(); + } + + public static Event getEvent(String name, CarpetScriptServer server) + { + if (byName.containsKey(name)) + { + return byName.get(name); + } + return server.events.customEvents.get(name); + } + + public static Event getOrCreateCustom(String name, CarpetScriptServer server) + { + Event event = getEvent(name, server); + if (event != null) + { + return event; + } + return new Event(name, server); + } + + public static void removeAllHostEvents(CarpetScriptHost host) + { + byName.values().forEach((e) -> e.handler.removeAllCalls(host)); + host.scriptServer().events.customEvents.values().forEach((e) -> e.handler.removeAllCalls(host)); + } + + public static void transferAllHostEventsToChild(CarpetScriptHost host) + { + byName.values().forEach((e) -> e.handler.createChildEvents(host)); + host.scriptServer().events.customEvents.values().forEach((e) -> e.handler.createChildEvents(host)); + } + + public static void clearAllBuiltinEvents() + { + byName.values().forEach(e -> e.handler.clearEverything()); + } + + // custom event constructor + private Event(String name, CarpetScriptServer server) + { + this.name = name; + this.handler = new CallbackList(1, false, false); + this.isPublic = true; + server.events.customEvents.put(name, this); + } + + //handle_event('event', function...) + //signal_event('event', player or null, args.... ) -> number of apps notified + + public boolean isNeeded() + { + return handler.callList.size() > 0; + } + + public boolean deprecated() + { + return false; + } + + //stubs for calls just to ease calls in vanilla code so they don't need to deal with scarpet value types + public void onTick(MinecraftServer server) + { + } + + public void onChunkEvent(ServerLevel world, ChunkPos chPos, boolean generated) + { + } + + public boolean onPlayerEvent(ServerPlayer player) + { + return false; + } + + public boolean onPlayerMessage(ServerPlayer player, String message) + { + return false; + } + + public void onPlayerStatistic(ServerPlayer player, Stat stat, int amount) + { + } + + public void onMountControls(ServerPlayer player, float strafeSpeed, float forwardSpeed, boolean jumping, boolean sneaking) + { + } + + public boolean onItemAction(ServerPlayer player, InteractionHand enumhand, ItemStack itemstack) + { + return false; + } + + public boolean onBlockAction(ServerPlayer player, BlockPos blockpos, Direction facing) + { + return false; + } + + public boolean onBlockHit(ServerPlayer player, InteractionHand enumhand, BlockHitResult hitRes) + { + return false; + } + + public boolean onBlockBroken(ServerPlayer player, BlockPos pos, BlockState previousBS) + { + return false; + } + + public boolean onBlockPlaced(ServerPlayer player, BlockPos pos, InteractionHand enumhand, ItemStack itemstack) + { + return false; + } + + public boolean onEntityHandAction(ServerPlayer player, Entity entity, InteractionHand enumhand) + { + return false; + } + + public void onHandAction(ServerPlayer player, InteractionHand enumhand) + { + } + + public void onEntityAction(Entity entity, boolean created) + { + } + + public void onDimensionChange(ServerPlayer player, Vec3 from, Vec3 to, ResourceKey fromDim, ResourceKey dimTo) + { + } + + public boolean onDamage(Entity target, float amount, DamageSource source) + { + return false; + } + + public boolean onRecipeSelected(ServerPlayer player, ResourceLocation recipe, boolean fullStack) + { + return false; + } + + public void onSlotSwitch(ServerPlayer player, int from, int to) + { + } + + public void onTrade(ServerPlayer player, Merchant merchant, MerchantOffer tradeOffer) + { + } + + public void onExplosion(ServerLevel world, Entity e, Supplier attacker, double x, double y, double z, float power, boolean createFire, List affectedBlocks, List affectedEntities, Explosion.BlockInteraction type) + { + } + + public void onWorldEvent(ServerLevel world, BlockPos pos) + { + } + + public void onWorldEventFlag(ServerLevel world, BlockPos pos, int flag) + { + } + + public void handleAny(Object... args) + { + } + + public void onCustomPlayerEvent(ServerPlayer player, Object... args) + { + if (handler.reqArgs != (args.length + 1)) + { + throw new InternalExpressionException("Expected " + handler.reqArgs + " arguments for " + name + ", got " + (args.length + 1)); + } + handler.call( + () -> { + List valArgs = new ArrayList<>(); + valArgs.add(EntityValue.of(player)); + for (Object o : args) + { + valArgs.add(ValueConversions.guess(player.serverLevel(), o)); + } + return valArgs; + }, player::createCommandSourceStack + ); + } + + public void onCustomWorldEvent(ServerLevel world, Object... args) + { + if (handler.reqArgs != args.length) + { + throw new InternalExpressionException("Expected " + handler.reqArgs + " arguments for " + name + ", got " + args.length); + } + handler.call( + () -> { + List valArgs = new ArrayList<>(); + for (Object o : args) + { + valArgs.add(ValueConversions.guess(world, o)); + } + return valArgs; + }, () -> world.getServer().createCommandSourceStack().withLevel(world) + ); + } + } + + + public CarpetEventServer(CarpetScriptServer scriptServer) + { + this.scriptServer = scriptServer; + Event.clearAllBuiltinEvents(); + } + + public void tick() + { + if (!scriptServer.server.tickRateManager().runsNormally()) + { + return; + } + Iterator eventIterator = scheduledCalls.iterator(); + List currentCalls = new ArrayList<>(); + while (eventIterator.hasNext()) + { + ScheduledCall call = eventIterator.next(); + call.dueTime--; + if (call.dueTime <= 0) + { + currentCalls.add(call); + eventIterator.remove(); + } + } + for (ScheduledCall call : currentCalls) + { + call.execute(); + } + + } + + public void scheduleCall(CarpetContext context, FunctionValue function, List args, long due) + { + scheduledCalls.add(new ScheduledCall(context, function, args, due)); + } + + public void runScheduledCall(BlockPos origin, CommandSourceStack source, String hostname, CarpetScriptHost host, FunctionValue udf, List argv) + { + if (hostname != null && !scriptServer.modules.containsKey(hostname)) // well - scheduled call app got unloaded + { + return; + } + try + { + host.callUDF(origin, source, udf, argv); + } + catch (NullPointerException | InvalidCallbackException | IntegrityException ignored) + { + } + } + + public CallbackResult runEventCall(CommandSourceStack sender, String hostname, String optionalTarget, FunctionValue udf, List argv) + { + CarpetScriptHost appHost = scriptServer.getAppHostByName(hostname); + // no such app + if (appHost == null) + { + return CallbackResult.FAIL; + } + // dummy call for player apps that reside on the global copy - do not run them, but report as passes. + if (appHost.isPerUser() && optionalTarget == null) + { + return CallbackResult.PASS; + } + ServerPlayer target = null; + if (optionalTarget != null) + { + target = sender.getServer().getPlayerList().getPlayerByName(optionalTarget); + if (target == null) + { + return CallbackResult.FAIL; + } + } + CommandSourceStack source = sender.withPermission(Vanilla.MinecraftServer_getRunPermissionLevel(sender.getServer())); + CarpetScriptHost executingHost = appHost.retrieveForExecution(sender, target); + if (executingHost == null) + { + return CallbackResult.FAIL; + } + try + { + Value returnValue = executingHost.callUDF(source, udf, argv); + return returnValue instanceof StringValue && returnValue.getString().equals("cancel") ? CallbackResult.CANCEL : CallbackResult.SUCCESS; + } + catch (NullPointerException | InvalidCallbackException | IntegrityException error) + { + CarpetScriptServer.LOG.error("Got exception when running event call ", error); + return CallbackResult.FAIL; + } + } + + public boolean addEventFromCommand(CommandSourceStack source, String event, String host, String funName) + { + Event ev = Event.getEvent(event, scriptServer); + if (ev == null) + { + return false; + } + boolean added = ev.handler.addFromExternal(source, host, funName, h -> onEventAddedToHost(ev, h), scriptServer); + if (added) + { + Carpet.Messenger_message(source, "gi Added " + funName + " to " + event); + } + return added; + } + + public void addBuiltInEvent(String event, ScriptHost host, FunctionValue function, List args) + { + // this is globals only + Event ev = Event.byName.get(event); + onEventAddedToHost(ev, host); + boolean success = ev.handler.addEventCallInternal(host, function, args == null ? NOARGS : args); + if (!success) + { + throw new InternalExpressionException("Global event " + event + " requires " + ev.handler.reqArgs + ", not " + (function.getNumParams() - ((args == null) ? 0 : args.size()))); + } + } + + public boolean handleCustomEvent(String event, CarpetScriptHost host, FunctionValue function, List args) + { + Event ev = Event.getOrCreateCustom(event, scriptServer); + onEventAddedToHost(ev, host); + return ev.handler.addEventCallInternal(host, function, args == null ? NOARGS : args); + } + + public int signalEvent(String event, CarpetContext cc, @Nullable ServerPlayer target, List callArgs) + { + Event ev = Event.getEvent(event, ((CarpetScriptHost) cc.host).scriptServer()); + return ev == null ? -1 : ev.handler.signal(cc.source(), target, callArgs); + } + + private void onEventAddedToHost(Event event, ScriptHost host) + { + if (event.deprecated()) + { + host.issueDeprecation(event.name + " event"); + } + event.handler.sortByPriority(this.scriptServer); + } + + public boolean removeEventFromCommand(CommandSourceStack source, String event, String funName) + { + Event ev = Event.getEvent(event, scriptServer); + if (ev == null) + { + Carpet.Messenger_message(source, "r Unknown event: " + event); + return false; + } + Callback.Signature call = Callback.fromString(funName); + ev.handler.removeEventCall(call.host, call.target, call.function); + // could verified if actually removed + Carpet.Messenger_message(source, "gi Removed event: " + funName + " from " + event); + return true; + } + + public boolean removeBuiltInEvent(String event, CarpetScriptHost host) + { + Event ev = Event.getEvent(event, host.scriptServer()); + if (ev == null) + { + return false; + } + ev.handler.removeAllCalls(host); + return true; + } + + public void removeBuiltInEvent(String event, CarpetScriptHost host, String funName) + { + Event ev = Event.getEvent(event, host.scriptServer()); + if (ev != null) + { + ev.handler.removeEventCall(host.getName(), host.user, funName); + } + } + + public void removeAllHostEvents(CarpetScriptHost host) + { + // remove event handlers + Event.removeAllHostEvents(host); + if (host.isPerUser()) + { + for (ScriptHost child : host.userHosts.values()) + { + Event.removeAllHostEvents((CarpetScriptHost) child); + } + } + // remove scheduled calls + scheduledCalls.removeIf(sc -> sc.host != null && sc.host.equals(host.getName())); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/script/CarpetExpression.java b/src/main/java/carpet/script/CarpetExpression.java new file mode 100644 index 0000000..8535812 --- /dev/null +++ b/src/main/java/carpet/script/CarpetExpression.java @@ -0,0 +1,144 @@ +package carpet.script; + +import carpet.script.annotation.AnnotationParser; +import carpet.script.api.Auxiliary; +import carpet.script.api.BlockIterators; +import carpet.script.api.Entities; +import carpet.script.api.Inventories; +import carpet.script.api.Monitoring; +import carpet.script.api.Scoreboards; +import carpet.script.api.Threading; +import carpet.script.api.WorldAccess; +import carpet.script.exception.CarpetExpressionException; +import carpet.script.exception.ExpressionException; +import carpet.script.external.Carpet; +import carpet.script.value.BlockValue; +import carpet.script.value.EntityValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; + +public class CarpetExpression +{ + private final CommandSourceStack source; + private final BlockPos origin; + private final Expression expr; + + // these are for extensions + public Expression getExpr() + { + return expr; + } + + public CommandSourceStack getSource() + { + return source; + } + + public BlockPos getOrigin() + { + return origin; + } + + public CarpetExpression(Module module, String expression, CommandSourceStack source, BlockPos origin) + { + this.origin = origin; + this.source = source; + this.expr = new Expression(expression); + this.expr.asAModule(module); + + WorldAccess.apply(this.expr); + Entities.apply(this.expr); + Inventories.apply(this.expr); + BlockIterators.apply(this.expr); + Auxiliary.apply(this.expr); + Threading.apply(this.expr); + Scoreboards.apply(this.expr); + Monitoring.apply(this.expr); + AnnotationParser.apply(this.expr); + Carpet.handleExtensionsAPI(this); + } + + public boolean fillAndScanCommand(ScriptHost host, int x, int y, int z) + { + CarpetScriptServer scriptServer = (CarpetScriptServer) host.scriptServer(); + if (scriptServer.stopAll) + { + return false; + } + try + { + Context context = new CarpetContext(host, source, origin). + with("x", (c, t) -> new NumericValue(x - origin.getX()).bindTo("x")). + with("y", (c, t) -> new NumericValue(y - origin.getY()).bindTo("y")). + with("z", (c, t) -> new NumericValue(z - origin.getZ()).bindTo("z")). + with("_", (c, t) -> new BlockValue(null, source.getLevel(), new BlockPos(x, y, z)).bindTo("_")); + Entity e = source.getEntity(); + if (e == null) + { + Value nullPlayer = Value.NULL.reboundedTo("p"); + context.with("p", (cc, tt) -> nullPlayer); + } + else + { + Value playerValue = new EntityValue(e).bindTo("p"); + context.with("p", (cc, tt) -> playerValue); + } + return scriptServer.events.handleEvents.getWhileDisabled(() -> this.expr.eval(context).getBoolean()); + } + catch (ExpressionException e) + { + throw new CarpetExpressionException(e.getMessage(), e.stack); + } + catch (ArithmeticException ae) + { + throw new CarpetExpressionException("Math doesn't compute... " + ae.getMessage(), null); + } + catch (StackOverflowError soe) + { + throw new CarpetExpressionException("Your thoughts are too deep", null); + } + } + + public Value scriptRunCommand(ScriptHost host, BlockPos pos) + { + CarpetScriptServer scriptServer = (CarpetScriptServer) host.scriptServer(); + if (scriptServer.stopAll) + { + throw new CarpetExpressionException("SCRIPTING PAUSED (unpause with /script resume)", null); + } + try + { + Context context = new CarpetContext(host, source, origin). + with("x", (c, t) -> new NumericValue(pos.getX() - origin.getX()).bindTo("x")). + with("y", (c, t) -> new NumericValue(pos.getY() - origin.getY()).bindTo("y")). + with("z", (c, t) -> new NumericValue(pos.getZ() - origin.getZ()).bindTo("z")); + Entity e = source.getEntity(); + if (e == null) + { + Value nullPlayer = Value.NULL.reboundedTo("p"); + context.with("p", (cc, tt) -> nullPlayer); + } + else + { + Value playerValue = new EntityValue(e).bindTo("p"); + context.with("p", (cc, tt) -> playerValue); + } + return scriptServer.events.handleEvents.getWhileDisabled(() -> this.expr.eval(context)); + } + catch (ExpressionException e) + { + throw new CarpetExpressionException(e.getMessage(), e.stack); + } + catch (ArithmeticException ae) + { + throw new CarpetExpressionException("Math doesn't compute... " + ae.getMessage(), null); + } + catch (StackOverflowError soe) + { + throw new CarpetExpressionException("Your thoughts are too deep", null); + } + } +} diff --git a/src/main/java/carpet/script/CarpetScriptHost.java b/src/main/java/carpet/script/CarpetScriptHost.java new file mode 100644 index 0000000..7a8d841 --- /dev/null +++ b/src/main/java/carpet/script/CarpetScriptHost.java @@ -0,0 +1,1217 @@ +package carpet.script; + +import carpet.script.api.Auxiliary; +import carpet.script.argument.FileArgument; +import carpet.script.argument.FunctionArgument; +import carpet.script.command.CommandArgument; +import carpet.script.command.CommandToken; +import carpet.script.exception.CarpetExpressionException; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.IntegrityException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.InvalidCallbackException; +import carpet.script.exception.LoadException; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.utils.AppStoreManager; +import carpet.script.value.EntityValue; +import carpet.script.value.FunctionValue; +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.gson.JsonElement; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nullable; +import java.math.BigInteger; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.lang.Math.max; +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +public class CarpetScriptHost extends ScriptHost +{ + public CommandSourceStack responsibleSource; + + private Tag globalState; + private int saveTimeout; + public boolean persistenceRequired; + public double eventPriority; + + public Map appConfig; + public Map appArgTypes; + + Predicate commandValidator; + boolean isRuleApp; + public AppStoreManager.StoreNode storeSource; + boolean hasCommand; + + private CarpetScriptHost(CarpetScriptServer server, @Nullable Module code, boolean perUser, ScriptHost parent, Map config, Map argTypes, Predicate commandValidator, boolean isRuleApp) + { + super(code, server, perUser, parent); + this.saveTimeout = 0; + persistenceRequired = true; + if (parent == null && code != null) // app, not a global host + { + globalState = loadState(); + } + else if (parent != null) + { + persistenceRequired = ((CarpetScriptHost) parent).persistenceRequired; + strict = parent.strict; + } + appConfig = config; + appArgTypes = argTypes; + this.commandValidator = commandValidator; + this.isRuleApp = isRuleApp; + storeSource = null; + } + + public static CarpetScriptHost create(CarpetScriptServer scriptServer, @Nullable Module module, boolean perPlayer, CommandSourceStack source, Predicate commandValidator, boolean isRuleApp, AppStoreManager.StoreNode storeSource) + { + CarpetScriptHost host = new CarpetScriptHost(scriptServer, module, perPlayer, null, Collections.emptyMap(), new HashMap<>(), commandValidator, isRuleApp); + // parse code and convert to expression + if (module != null) + { + try + { + host.setChatErrorSnooper(source); + CarpetExpression ex = new CarpetExpression(host.main, module.code(), source, new BlockPos(0, 0, 0)); + ex.getExpr().asATextSource(); + host.storeSource = storeSource; + ex.scriptRunCommand(host, BlockPos.containing(source.getPosition())); + } + catch (CarpetExpressionException e) + { + host.handleErrorWithStack("Error while evaluating expression", e); + throw new LoadException(); + } + catch (ArithmeticException ae) // is this branch ever reached? Seems like arithmetic exceptions are converted to CEEs earlier + { + host.handleErrorWithStack("Math doesn't compute", ae); + throw new LoadException(); + } + catch (StackOverflowError soe) + { + host.handleErrorWithStack("Your thoughts are too deep", soe); + } + finally + { + host.storeSource = null; + } + } + return host; + } + + private static int execute(CommandContext ctx, String hostName, FunctionArgument funcSpec, List paramNames) throws CommandSyntaxException + { + Runnable token = Carpet.startProfilerSection("Scarpet command"); + CarpetScriptServer scriptServer = Vanilla.MinecraftServer_getScriptServer(ctx.getSource().getServer()); + CarpetScriptHost cHost = scriptServer.modules.get(hostName).retrieveOwnForExecution(ctx.getSource()); + List argNames = funcSpec.function.getArguments(); + if ((argNames.size() - funcSpec.args.size()) != paramNames.size()) + { + throw new SimpleCommandExceptionType(Component.literal("Target function " + funcSpec.function.getPrettyString() + " as wrong number of arguments, required " + paramNames.size() + ", found " + argNames.size() + " with " + funcSpec.args.size() + " provided")).create(); + } + List args = new ArrayList<>(argNames.size()); + for (String s : paramNames) + { + args.add(CommandArgument.getValue(ctx, s, cHost)); + } + args.addAll(funcSpec.args); + Value response = cHost.handleCommand(ctx.getSource(), funcSpec.function, args); + int intres = (int) response.readInteger(); + token.run(); + return intres; + } + + public LiteralArgumentBuilder addPathToCommand( + LiteralArgumentBuilder command, + List path, + FunctionArgument functionSpec + ) throws CommandSyntaxException + { + String hostName = main.name(); + List commandArgs = path.stream().filter(t -> t.isArgument).map(t -> t.surface).collect(Collectors.toList()); + if (commandArgs.size() != (functionSpec.function.getNumParams() - functionSpec.args.size())) + { + throw CommandArgument.error("Number of parameters in function " + functionSpec.function.fullName() + " doesn't match parameters for a command"); + } + if (path.isEmpty()) + { + return command.executes((c) -> execute(c, hostName, functionSpec, Collections.emptyList())); + } + List reversedPath = new ArrayList<>(path); + Collections.reverse(reversedPath); + ArgumentBuilder argChain = reversedPath.get(0).getCommandNode(this).executes(c -> execute(c, hostName, functionSpec, commandArgs)); + for (int i = 1; i < reversedPath.size(); i++) + { + argChain = reversedPath.get(i).getCommandNode(this).then(argChain); + } + return command.then(argChain); + } + + public LiteralArgumentBuilder getNewCommandTree( + List, FunctionArgument>> entries, Predicate useValidator + ) throws CommandSyntaxException + { + String hostName = main.name(); + Predicate configValidator = getCommandConfigPermissions(); + LiteralArgumentBuilder command = literal(hostName). + requires((player) -> useValidator.test(player) && configValidator.test(player)); + for (Pair, FunctionArgument> commandData : entries) + { + command = this.addPathToCommand(command, commandData.getKey(), commandData.getValue()); + } + return command; + } + + public Predicate getCommandConfigPermissions() throws CommandSyntaxException + { + Value confValue = appConfig.get(StringValue.of("command_permission")); + if (confValue == null) + { + return s -> true; + } + if (confValue instanceof final NumericValue number) + { + int level = number.getInt(); + if (level < 1 || level > 4) + { + throw CommandArgument.error("Numeric permission level for custom commands should be between 1 and 4"); + } + return s -> s.hasPermission(level); + } + if (!(confValue instanceof final FunctionValue fun)) + { + String perm = confValue.getString().toLowerCase(Locale.ROOT); + return switch (perm) { + case "ops" -> s -> s.hasPermission(2); + case "server" -> s -> !(s.getEntity() instanceof ServerPlayer); + case "players" -> s -> s.getEntity() instanceof ServerPlayer; + case "all" -> s -> true; + default -> throw CommandArgument.error("Unknown command permission: " + perm); + }; + } + if (fun.getNumParams() != 1) + { + throw CommandArgument.error("Custom command permission function should expect 1 argument"); + } + String hostName = getName(); + return s -> { + try + { + Runnable token = Carpet.startProfilerSection("Scarpet command"); + CarpetScriptHost cHost = scriptServer().modules.get(hostName).retrieveOwnForExecution(s); + Value response = cHost.handleCommand(s, fun, Collections.singletonList( + (s.getEntity() instanceof ServerPlayer) ? new EntityValue(s.getEntity()) : Value.NULL) + ); + boolean res = response.getBoolean(); + token.run(); + return res; + } + catch (CommandSyntaxException e) + { + Carpet.Messenger_message(s, "rb Unable to run app command: " + e.getMessage()); + return false; + } + }; + } + + @Override + protected ScriptHost duplicate() + { + return new CarpetScriptHost(scriptServer(), main, false, this, appConfig, appArgTypes, commandValidator, isRuleApp); + } + + @Override + protected void setupUserHost(ScriptHost host) + { + super.setupUserHost(host); + // transfer Events + CarpetScriptHost child = (CarpetScriptHost) host; + CarpetEventServer.Event.transferAllHostEventsToChild(child); + FunctionValue onStart = child.getFunction("__on_start"); + if (onStart != null) + { + child.callNow(onStart, Collections.emptyList()); + } + } + + @Override + public void addUserDefinedFunction(Context ctx, Module module, String funName, FunctionValue function) + { + super.addUserDefinedFunction(ctx, module, funName, function); + 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. + { + // this is nasty, we have the host and function, yet we add it via names, but hey - works for now + String event = funName.replaceFirst("__on_", ""); + if (CarpetEventServer.Event.byName.containsKey(event)) + { + scriptServer().events.addBuiltInEvent(event, this, function, null); + } + } + else if (funName.equals("__config")) + { + // needs to be added as we read the code, cause other events may be affected. + if (!readConfig()) + { + throw new InternalExpressionException("Invalid app config (via '__config()' function)"); + } + } + } + } + + private boolean readConfig() + { + try + { + FunctionValue configFunction = getFunction("__config"); + if (configFunction == null) + { + return false; + } + Value ret = callNow(configFunction, Collections.emptyList()); + if (!(ret instanceof final MapValue map)) + { + return false; + } + Map config = map.getMap(); + setPerPlayer(config.getOrDefault(new StringValue("scope"), new StringValue("player")).getString().equalsIgnoreCase("player")); + persistenceRequired = config.getOrDefault(new StringValue("stay_loaded"), Value.TRUE).getBoolean(); + strict = config.getOrDefault(StringValue.of("strict"), Value.FALSE).getBoolean(); + eventPriority = config.getOrDefault(new StringValue("event_priority"), Value.ZERO).readDoubleNumber(); + // check requires + Value loadRequirements = config.get(new StringValue("requires")); + if (loadRequirements instanceof final FunctionValue functionValue) + { + Value reqResult = callNow(functionValue, Collections.emptyList()); + if (reqResult.getBoolean()) // != false or null + { + throw new LoadException(reqResult.getString()); + } + } + else + { + checkModVersionRequirements(loadRequirements); + } + if (storeSource != null) + { + Value resources = config.get(new StringValue("resources")); + if (resources != null) + { + if (!(resources instanceof final ListValue list)) + { + throw new InternalExpressionException("App resources not defined as a list"); + } + for (Value resource : list.getItems()) + { + AppStoreManager.addResource(this, storeSource, resource); + } + } + Value libraries = config.get(new StringValue("libraries")); + if (libraries != null) + { + if (!(libraries instanceof final ListValue list)) + { + throw new InternalExpressionException("App libraries not defined as a list"); + } + for (Value library : list.getItems()) + { + AppStoreManager.addLibrary(this, storeSource, library); + } + } + } + appConfig = config; + } + catch (NullPointerException ignored) + { + return false; + } + return true; + } + + static class ListComparator> implements Comparator, ?>> + { + @Override + public int compare(Pair, ?> p1, Pair, ?> p2) + { + List o1 = p1.getKey(); + List o2 = p2.getKey(); + for (int i = 0; i < Math.min(o1.size(), o2.size()); i++) + { + int c = o1.get(i).compareTo(o2.get(i)); + if (c != 0) + { + return c; + } + } + return Integer.compare(o1.size(), o2.size()); + } + } + + // Used to ensure app gets marked as holding command from a central place + private void registerCommand(LiteralArgumentBuilder command) + { + scriptServer().server.getCommands().getDispatcher().register(command); + hasCommand = true; + } + + public void readCustomArgumentTypes() throws CommandSyntaxException + { + // read custom arguments + Value arguments = appConfig.get(StringValue.of("arguments")); + if (arguments != null) + { + if (!(arguments instanceof final MapValue map)) + { + throw CommandArgument.error("'arguments' element in config should be a map"); + } + appArgTypes.clear(); + for (Map.Entry typeData : map.getMap().entrySet()) + { + String argument = typeData.getKey().getString(); + Value spec = typeData.getValue(); + if (!(spec instanceof final MapValue specMap)) + { + throw CommandArgument.error("Spec for '" + argument + "' should be a map"); + } + Map specData = specMap.getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); + appArgTypes.put(argument, CommandArgument.buildFromConfig(argument, specData, this)); + } + } + } + + public Boolean addAppCommands(Consumer notifier) + { + try + { + readCustomArgumentTypes(); + } + catch (CommandSyntaxException e) + { + notifier.accept(Carpet.Messenger_compose("r Error when handling of setting up custom argument types: " + e.getMessage())); + return false; + } + if (appConfig.get(StringValue.of("commands")) != null) + { + if (scriptServer().isInvalidCommandRoot(getName())) + { + notifier.accept(Carpet.Messenger_compose("g A command with the app's name already exists in vanilla or an installed mod.")); + return null; + } + try + { + LiteralArgumentBuilder command = readCommands(commandValidator); + if (command != null) + { + registerCommand(command); + return true; + } + return false; + } + catch (CommandSyntaxException cse) + { + // failed + notifier.accept(Carpet.Messenger_compose("r Failed to build command system: ", cse.getRawMessage())); + return null; + } + + } + return addLegacyCommand(notifier); + } + + public void checkModVersionRequirements(Value reqs) + { + if (reqs == null) + { + return; + } + if (!(reqs instanceof final MapValue map)) + { + throw new InternalExpressionException("`requires` field must be a map of mod dependencies or a function to be executed"); + } + + Map requirements = map.getMap(); + for (Entry requirement : requirements.entrySet()) + { + String requiredModId = requirement.getKey().getString(); + String stringPredicate = requirement.getValue().getString(); + Carpet.assertRequirementMet(this, requiredModId, stringPredicate); + } + } + + private Boolean addLegacyCommand(Consumer notifier) + { + if (main == null || getFunction("__command") == null) + { + return false; + } + + if (scriptServer().isInvalidCommandRoot(getName())) + { + notifier.accept(Carpet.Messenger_compose("g A command with the app's name already exists in vanilla or an installed mod.")); + return null; + } + + Predicate configValidator; + try + { + configValidator = getCommandConfigPermissions(); + } + catch (CommandSyntaxException e) + { + notifier.accept(Carpet.Messenger_compose("rb " + e.getMessage())); + return null; + } + String hostName = getName(); + LiteralArgumentBuilder command = literal(hostName). + requires((player) -> commandValidator.test(player) && configValidator.test(player)). + executes((c) -> + { + CarpetScriptHost targetHost = scriptServer().modules.get(hostName).retrieveOwnForExecution(c.getSource()); + Value response = targetHost.handleCommandLegacy(c.getSource(), "__command", null, ""); + if (!response.isNull()) + { + Carpet.Messenger_message(c.getSource(), "gi " + response.getString()); + } + return (int) response.readInteger(); + }); + + boolean hasTypeSupport = appConfig.getOrDefault(StringValue.of("legacy_command_type_support"), Value.FALSE).getBoolean(); + + for (String function : globalFunctionNames(main, s -> !s.startsWith("_")).sorted().collect(Collectors.toList())) + { + if (hasTypeSupport) + { + try + { + FunctionValue functionValue = getFunction(function); + command = addPathToCommand( + command, + CommandToken.parseSpec(CommandToken.specFromSignature(functionValue), this), + FunctionArgument.fromCommandSpec(this, functionValue) + ); + } + catch (CommandSyntaxException e) + { + return false; + } + } + else + { + command = command. + then(literal(function). + requires((player) -> scriptServer().modules.get(hostName).getFunction(function) != null). + executes((c) -> { + CarpetScriptHost targetHost = scriptServer().modules.get(hostName).retrieveOwnForExecution(c.getSource()); + Value response = targetHost.handleCommandLegacy(c.getSource(), function, null, ""); + if (!response.isNull()) + { + Carpet.Messenger_message(c.getSource(), "gi " + response.getString()); + } + return (int) response.readInteger(); + }). + then(argument("args...", StringArgumentType.greedyString()). + executes((c) -> { + CarpetScriptHost targetHost = scriptServer().modules.get(hostName).retrieveOwnForExecution(c.getSource()); + Value response = targetHost.handleCommandLegacy(c.getSource(), function, null, StringArgumentType.getString(c, "args...")); + if (!response.isNull()) + { + Carpet.Messenger_message(c.getSource(), "gi " + response.getString()); + } + return (int) response.readInteger(); + }))); + } + } + registerCommand(command); + return true; + } + + public LiteralArgumentBuilder readCommands(Predicate useValidator) throws CommandSyntaxException + { + Value commands = appConfig.get(StringValue.of("commands")); + + if (commands == null) + { + return null; + } + if (!(commands instanceof final MapValue map)) + { + throw CommandArgument.error("'commands' element in config should be a map"); + } + List, FunctionArgument>> commandEntries = new ArrayList<>(); + + for (Map.Entry commandsData : map.getMap().entrySet().stream().sorted(Entry.comparingByKey()).toList()) + { + List elements = CommandToken.parseSpec(commandsData.getKey().getString(), this); + FunctionArgument funSpec = FunctionArgument.fromCommandSpec(this, commandsData.getValue()); + commandEntries.add(Pair.of(elements, funSpec)); + } + commandEntries.sort(new ListComparator<>()); + if (!appConfig.getOrDefault(StringValue.of("allow_command_conflicts"), Value.FALSE).getBoolean()) + { + for (int i = 0; i < commandEntries.size() - 1; i++) + { + List first = commandEntries.get(i).getKey(); + List other = commandEntries.get(i + 1).getKey(); + int checkSize = Math.min(first.size(), other.size()); + for (int t = 0; t < checkSize; t++) + { + CommandToken tik = first.get(t); + CommandToken tok = other.get(t); + if (tik.isArgument && tok.isArgument && !tik.surface.equals(tok.surface)) + { + throw CommandArgument.error("Conflicting commands: \n" + + " - [" + first.stream().map(tt -> tt.surface).collect(Collectors.joining(" ")) + "] at " + tik.surface + "\n" + + " - [" + other.stream().map(tt -> tt.surface).collect(Collectors.joining(" ")) + "] at " + tok.surface + "\n"); + } + if (!tik.equals(tok)) + { + break; + } + } + } + } + return this.getNewCommandTree(commandEntries, useValidator); + } + + @Override + protected Module getModuleOrLibraryByName(String name) + { + Module module = scriptServer().getModule(name, true); + if (module == null) + { + throw new InternalExpressionException("Unable to locate package: " + name); + } + return module; + } + + @Override + protected void runModuleCode(Context c, Module module) + { + CarpetContext cc = (CarpetContext) c; + CarpetExpression ex = new CarpetExpression(module, module.code(), cc.source(), cc.origin()); + ex.getExpr().asATextSource(); + ex.scriptRunCommand(this, cc.origin()); + } + + @Override + public void delFunction(Module module, String funName) + { + super.delFunction(module, funName); + // mcarpet + if (funName.startsWith("__on_")) + { + // this is nasty, we have the host and function, yet we add it via names, but hey - works for now + String event = funName.replaceFirst("__on_", ""); + scriptServer().events.removeBuiltInEvent(event, this, funName); + } + } + + public CarpetScriptHost retrieveForExecution(CommandSourceStack source, ServerPlayer player) + { + CarpetScriptHost target = null; + if (!perUser) + { + target = this; + } + else if (player != null) + { + target = (CarpetScriptHost) retrieveForExecution(player.getScoreboardName()); + } + if (target != null && target.errorSnooper == null) + { + target.setChatErrorSnooper(source); + } + return target; + } + + public CarpetScriptHost retrieveOwnForExecution(CommandSourceStack source) throws CommandSyntaxException + { + if (!perUser) + { + if (errorSnooper == null) + { + setChatErrorSnooper(source); + } + return this; + } + // user based + ServerPlayer player = source.getPlayer(); + if (player == null) + { + throw new SimpleCommandExceptionType(Component.literal("Cannot run player based apps without the player context")).create(); + } + CarpetScriptHost userHost = (CarpetScriptHost) retrieveForExecution(player.getScoreboardName()); + if (userHost.errorSnooper == null) + { + userHost.setChatErrorSnooper(source); + } + return userHost; + } + + public Value handleCommandLegacy(CommandSourceStack source, String call, List coords, String arg) + { + try + { + Runnable token = Carpet.startProfilerSection("Scarpet command"); + Value res = callLegacy(source, call, coords, arg); + token.run(); + return res; + } + catch (CarpetExpressionException exc) + { + handleErrorWithStack("Error while running custom command", exc); + } + catch (ArithmeticException ae) + { + handleErrorWithStack("Math doesn't compute", ae); + } + catch (StackOverflowError soe) + { + handleErrorWithStack("Your thoughts are too deep", soe); + } + return Value.NULL; + } + + public Value handleCommand(CommandSourceStack source, FunctionValue function, List args) + { + try + { + return scriptServer().events.handleEvents.getWhileDisabled(() -> call(source, function, args)); + } + catch (CarpetExpressionException exc) + { + handleErrorWithStack("Error while running custom command", exc); + } + catch (ArithmeticException ae) + { + handleErrorWithStack("Math doesn't compute", ae); + } + catch (StackOverflowError soe) + { + handleErrorWithStack("Your thoughts are too deep", soe); + } + return Value.NULL; + } + + public Value callLegacy(CommandSourceStack source, String call, List coords, String arg) + { + if (scriptServer().stopAll) + { + throw new CarpetExpressionException("SCARPET PAUSED (unpause with /script resume)", null); + } + FunctionValue function = getFunction(call); + if (function == null) + { + throw new CarpetExpressionException("Couldn't find function '" + call + "' in app '" + this.getVisualName() + "'", null); + } + List argv = new ArrayList<>(); + if (coords != null) + { + for (Integer i : coords) + { + argv.add((c, t) -> new NumericValue(i)); + } + } + String sign = ""; + for (Tokenizer.Token tok : Tokenizer.simplepass(arg)) + { + switch (tok.type) + { + case VARIABLE: + LazyValue variable = getGlobalVariable(tok.surface); + if (variable != null) + { + argv.add(variable); + } + break; + case STRINGPARAM: + argv.add((c, t) -> new StringValue(tok.surface)); + sign = ""; + break; + + case LITERAL: + try + { + String finalSign = sign; + argv.add((c, t) -> new NumericValue(finalSign + tok.surface)); + sign = ""; + } + catch (NumberFormatException exception) + { + throw new CarpetExpressionException("Fail: " + sign + tok.surface + " seems like a number but it is" + + " not a number. Use quotes to ensure its a string", null); + } + break; + case HEX_LITERAL: + try + { + String finalSign = sign; + argv.add((c, t) -> new NumericValue(new BigInteger(finalSign + tok.surface.substring(2), 16).doubleValue())); + sign = ""; + } + catch (NumberFormatException exception) + { + throw new CarpetExpressionException("Fail: " + sign + tok.surface + " seems like a number but it is" + + " not a number. Use quotes to ensure its a string", null); + } + break; + case OPERATOR, UNARY_OPERATOR: + if ((tok.surface.equals("-") || tok.surface.equals("-u")) && sign.isEmpty()) + { + sign = "-"; + } + else + { + throw new CarpetExpressionException("Fail: operators, like " + tok.surface + " are not " + + "allowed in invoke", null); + } + break; + case FUNCTION: + throw new CarpetExpressionException("Fail: passing functions like " + tok.surface + "() to invoke is " + + "not allowed", null); + case OPEN_PAREN, COMMA, CLOSE_PAREN, MARKER: + throw new CarpetExpressionException("Fail: " + tok.surface + " is not allowed in invoke", null); + } + } + List args = function.getArguments(); + if (argv.size() != args.size()) + { + String error = "Fail: stored function " + call + " takes " + args.size() + " arguments, not " + argv.size() + ":\n"; + for (int i = 0; i < max(argv.size(), args.size()); i++) + { + error += (i < args.size() ? args.get(i) : "??") + " => " + (i < argv.size() ? argv.get(i).evalValue(null).getString() : "??") + "\n"; + } + throw new CarpetExpressionException(error, null); + } + try + { + // TODO: this is just for now - invoke would be able to invoke other hosts scripts + assertAppIntegrity(function.getModule()); + Context context = new CarpetContext(this, source); + return scriptServer().events.handleEvents.getWhileDisabled(() -> function.getExpression().evalValue( + () -> function.lazyEval(context, Context.VOID, function.getExpression(), function.getToken(), argv), + context, + Context.VOID + )); + } + catch (ExpressionException e) + { + throw new CarpetExpressionException(e.getMessage(), e.stack); + } + } + + public Value call(CommandSourceStack source, FunctionValue function, List argv) + { + if (scriptServer().stopAll) + { + throw new CarpetExpressionException("SCARPET PAUSED (unpause with /script resume)", null); + } + + List args = function.getArguments(); + if (argv.size() != args.size()) + { + String error = "Fail: stored function " + function.getPrettyString() + " takes " + args.size() + " arguments, not " + argv.size() + ":\n"; + for (int i = 0; i < max(argv.size(), args.size()); i++) + { + error += (i < args.size() ? args.get(i) : "??") + " => " + (i < argv.size() ? argv.get(i).getString() : "??") + "\n"; + } + throw new CarpetExpressionException(error, null); + } + try + { + assertAppIntegrity(function.getModule()); + Context context = new CarpetContext(this, source); + return function.getExpression().evalValue( + () -> function.execute(context, Context.VOID, function.getExpression(), function.getToken(), argv, null), + context, + Context.VOID + ); + } + catch (ExpressionException e) + { + throw new CarpetExpressionException(e.getMessage(), e.stack); + } + } + + public Value callUDF(CommandSourceStack source, FunctionValue fun, List argv) throws InvalidCallbackException, IntegrityException + { + return callUDF(BlockPos.ZERO, source, fun, argv); + } + + public Value callUDF(BlockPos origin, CommandSourceStack source, FunctionValue fun, List argv) throws InvalidCallbackException, IntegrityException + { + if (scriptServer().stopAll) + { + return Value.NULL; + } + try + { // cause we can't throw checked exceptions in lambda. Left if be until need to handle these more gracefully + fun.assertArgsOk(argv, (b) -> { + throw new InternalExpressionException(""); + }); + } + catch (InternalExpressionException ignored) + { + throw new InvalidCallbackException(); + } + try + { + assertAppIntegrity(fun.getModule()); + Context context = new CarpetContext(this, source, origin); + return fun.getExpression().evalValue( + () -> fun.execute(context, Context.VOID, fun.getExpression(), fun.getToken(), argv, null), + context, + Context.VOID); + } + catch (ExpressionException e) + { + handleExpressionException("Callback failed", e); + } + return Value.NULL; + } + + public Value callNow(FunctionValue fun, List arguments) + { + ServerPlayer player = (user == null) ? null : scriptServer().server.getPlayerList().getPlayerByName(user); + CommandSourceStack source = (player != null) ? player.createCommandSourceStack() : scriptServer().server.createCommandSourceStack(); + return scriptServer().events.handleEvents.getWhileDisabled(() -> { + try + { + return callUDF(source, fun, arguments); + } + catch (InvalidCallbackException ignored) + { + return Value.NULL; + } + }); + } + + + @Override + public void onClose() + { + super.onClose(); + FunctionValue closing = getFunction("__on_close"); + if (closing != null && (parent != null || !isPerUser())) + // either global instance of a global task, or + // user host in player scoped app + { + callNow(closing, Collections.emptyList()); + } + if (user == null) + { + + String markerName = Auxiliary.MARKER_STRING + "_" + ((getName() == null) ? "" : getName()); + for (ServerLevel world : scriptServer().server.getAllLevels()) + { + for (Entity e : world.getEntities(EntityType.ARMOR_STAND, (as) -> as.getTags().contains(markerName))) + { + e.discard(); + } + } + if (this.saveTimeout > 0) + { + dumpState(); + } + } + } + + private void dumpState() + { + Module.saveData(main, globalState, this.scriptServer()); + } + + private Tag loadState() + { + return Module.getData(main, this.scriptServer()); + } + + public Tag readFileTag(FileArgument fdesc) + { + if (isDefaultApp() && !fdesc.isShared) + { + return null; + } + if (fdesc.resource != null) + { + return fdesc.getNbtData(main); + } + if (parent == null) + { + return globalState; + } + return ((CarpetScriptHost) parent).globalState; + } + + public boolean writeTagFile(Tag tag, FileArgument fdesc) + { + if (isDefaultApp() && !fdesc.isShared) + { + return false; // if belongs to an app, cannot be default host. + } + + if (fdesc.resource != null) + { + return fdesc.saveNbtData(main, tag); + } + + CarpetScriptHost responsibleHost = (parent != null) ? (CarpetScriptHost) parent : this; + responsibleHost.globalState = tag; + if (responsibleHost.saveTimeout == 0) + { + responsibleHost.dumpState(); + responsibleHost.saveTimeout = 200; + } + return true; + } + + public boolean removeResourceFile(FileArgument fdesc) + { + return (!isDefaultApp() || fdesc.isShared) && fdesc.dropExistingFile(main); // + } + + public boolean appendLogFile(FileArgument fdesc, List data) + { + return (!isDefaultApp() || fdesc.isShared) && fdesc.appendToTextFile(main, data); // if belongs to an app, cannot be default host. + } + + public List readTextResource(FileArgument fdesc) + { + return isDefaultApp() && !fdesc.isShared ? null : fdesc.listFile(main); + } + + public JsonElement readJsonFile(FileArgument fdesc) + { + return isDefaultApp() && !fdesc.isShared ? null : fdesc.readJsonFile(main); + } + + public Stream listFolder(FileArgument fdesc) + { + return isDefaultApp() && !fdesc.isShared ? null : fdesc.listFolder(main); + } + + public boolean applyActionForResource(String path, boolean shared, Consumer action) + { + FileArgument fdesc = FileArgument.resourceFromPath(this, path, FileArgument.Reason.CREATE, shared); + return fdesc.findPathAndApply(main, action); + } + + public void tick() + { + if (this.saveTimeout > 0) + { + this.saveTimeout--; + if (this.saveTimeout == 0) + { + dumpState(); + } + } + } + + public void setChatErrorSnooper(CommandSourceStack source) + { + responsibleSource = source; + errorSnooper = (expr, /*Nullable*/ token, ctx, message) -> + { + if (!source.isPlayer()) + { + return null; + } + + String shebang = message + " in " + expr.getModuleName(); + if (token != null) + { + String[] lines = expr.getCodeString().split("\n"); + + if (lines.length > 1) + { + shebang += " at line " + (token.lineno + 1) + ", pos " + (token.linepos + 1); + } + else + { + shebang += " at pos " + (token.pos + 1); + } + Carpet.Messenger_message(source, "r " + shebang); + if (lines.length > 1 && token.lineno > 0) + { + Carpet.Messenger_message(source, withLocals("l", lines[token.lineno - 1], ctx)); + } + Carpet.Messenger_message(source, withLocals("l", lines[token.lineno].substring(0, token.linepos), ctx), "r HERE>> ", + withLocals("l", lines[token.lineno].substring(token.linepos), ctx)); + if (lines.length > 1 && token.lineno < lines.length - 1) + { + Carpet.Messenger_message(source, withLocals("l", lines[token.lineno + 1], ctx)); + } + } + else + { + Carpet.Messenger_message(source, "r " + shebang); + } + return new ArrayList<>(); + }; + } + + /** + *

Creates a {@link Component} using {@link Messenger} that has the locals in the {@code line} snippet with a hover over + * tooltip with the value of the local at that location

+ * + * @param line The line to find references to locals on + * @param context The {@link Context} to extract the locals from + * @param format The format to apply to each part of the line, without the trailing space + * @return A BaseText of the given line with the given format, that is visibly the same as passing those to Messenger, but with references to the + * locals in the {@link Context} with a hover over tooltip text + * @implNote The implementation of this method is far from perfect, and won't detect actual references to variables, but try to find the strings + * and add the hover effect to anything that equals to any variable name, so short variable names may appear on random positions + */ + private static Component withLocals(String format, String line, Context context) + { + format += " "; + List stringsToFormat = new ArrayList<>(); + TreeMap posToLocal = new TreeMap<>(); //Holds whether a local variable name is found at a specific index + for (String local : context.variables.keySet()) + { + int pos = line.indexOf(local); + while (pos != -1) + { + posToLocal.merge(pos, local, (existingLocal, newLocal) -> + { + // Prefer longer variable names at the same position, since else single chars everywhere + return newLocal.length() > existingLocal.length() ? local : existingLocal; + }); + pos = line.indexOf(local, pos + 1); + } + } + int lastPos = 0; + for (Entry foundLocal : posToLocal.entrySet()) + { + if (foundLocal.getKey() < lastPos) // system isn't perfect: part of another local + { + continue; + } + stringsToFormat.add(format + line.substring(lastPos, foundLocal.getKey())); + stringsToFormat.add(format + foundLocal.getValue()); + Value val = context.variables.get(foundLocal.getValue()).evalValue(context); + String type = val.getTypeString(); + String value; + try + { + value = val.getPrettyString(); + } + catch (StackOverflowError e) + { + value = "Exception while rendering variable, there seems to be a recursive reference in there"; + } + stringsToFormat.add("^ Value of '" + foundLocal.getValue() + "' at position (" + type + "): \n" + + value); + lastPos = foundLocal.getKey() + foundLocal.getValue().length(); + } + if (line.length() != lastPos) + { + stringsToFormat.add(format + line.substring(lastPos)); + } + return Carpet.Messenger_compose(stringsToFormat.toArray()); + } + + @Override + public void resetErrorSnooper() + { + responsibleSource = null; + super.resetErrorSnooper(); + } + + public void handleErrorWithStack(String intro, Throwable exception) + { + if (responsibleSource != null) + { + if (exception instanceof final CarpetExpressionException cee) + { + cee.printStack(responsibleSource); + } + String message = exception.getMessage(); + Carpet.Messenger_message(responsibleSource, "r " + intro + ((message == null || message.isEmpty()) ? "" : ": " + message)); + } + else + { + CarpetScriptServer.LOG.error(intro + ": " + exception.getMessage()); + } + } + + @Override + public synchronized void handleExpressionException(String message, ExpressionException exc) + { + handleErrorWithStack(message, new CarpetExpressionException(exc.getMessage(), exc.stack)); + } + + /** + * @deprecated Use {@link #scriptServer()} instead + */ + @Deprecated(forRemoval = true) + public CarpetScriptServer getScriptServer() + { + return scriptServer(); + } + + @Override + public CarpetScriptServer scriptServer() + { + return (CarpetScriptServer) super.scriptServer(); + } + + @Override + public boolean issueDeprecation(String feature) + { + if (super.issueDeprecation(feature)) + { + Carpet.Messenger_message(responsibleSource, "rb App '" +getVisualName() + "' uses '" + feature + "', which is deprecated for removal. Check the docs for a replacement"); + return true; + } + return false; + } + + @Override + public boolean canSynchronouslyExecute() + { + return !scriptServer().server.isSameThread(); + } +} diff --git a/src/main/java/carpet/script/CarpetScriptServer.java b/src/main/java/carpet/script/CarpetScriptServer.java new file mode 100644 index 0000000..5345cea --- /dev/null +++ b/src/main/java/carpet/script/CarpetScriptServer.java @@ -0,0 +1,509 @@ +package carpet.script; + +import carpet.script.annotation.AnnotationParser; +import carpet.script.api.Auxiliary; +import carpet.script.api.BlockIterators; +import carpet.script.api.Entities; +import carpet.script.api.Inventories; +import carpet.script.api.Scoreboards; +import carpet.script.api.WorldAccess; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.LoadException; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.language.Arithmetic; +import carpet.script.language.ControlFlow; +import carpet.script.language.DataStructures; +import carpet.script.language.Functions; +import carpet.script.language.Loops; +import carpet.script.language.Sys; +import carpet.script.language.Threading; +import carpet.script.utils.AppStoreManager; +import carpet.script.value.FunctionValue; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.CommandNode; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.storage.LevelResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static carpet.script.CarpetEventServer.Event.PLAYER_CONNECTS; +import static carpet.script.CarpetEventServer.Event.PLAYER_DISCONNECTS; + +public class CarpetScriptServer extends ScriptServer +{ + //make static for now, but will change that later: + public static final Logger LOG = LoggerFactory.getLogger("Scarpet"); + public final MinecraftServer server; + public CarpetScriptHost globalHost; + public Map modules; + public Set unloadableModules; + public long tickStart; + public boolean stopAll; + public int tickDepth; + private Set holyMoly; + public CarpetEventServer events; + + private static final List bundledModuleData = new ArrayList<>(); + private static final List ruleModuleData = new ArrayList<>(); + + /** + * Registers a Scarpet App to be always available under the {@code /script load} list. + * + * @param app The {@link Module} of the app + * @see Module#fromJarPath(String, String, boolean) + */ + public static void registerBuiltInApp(Module app) + { + bundledModuleData.add(app); + } + + /** + * Registers a Scarpet App to be used as a Rule App (to be controlled with the value of a Carpet rule). + * Libraries should be registered with {@link #registerBuiltInScript(BundledModule)} instead + * + * @param app The {@link Module} of the app. + * @see Module#fromJarPath(String, String, boolean) + */ + public static void registerSettingsApp(Module app) + { + ruleModuleData.add(app); + } + + static + { + registerBuiltInApp(Module.carpetNative("camera", false)); + registerBuiltInApp(Module.carpetNative("overlay", false)); + registerBuiltInApp(Module.carpetNative("event_test", false)); + registerBuiltInApp(Module.carpetNative("stats_test", false)); + registerBuiltInApp(Module.carpetNative("math", true)); + registerBuiltInApp(Module.carpetNative("chunk_display", false)); + registerBuiltInApp(Module.carpetNative("ai_tracker", false)); + registerBuiltInApp(Module.carpetNative("draw_beta", false)); + registerBuiltInApp(Module.carpetNative("shapes", true)); + registerBuiltInApp(Module.carpetNative("distance_beta", false)); + } + + public CarpetScriptServer(MinecraftServer server) + { + this.server = server; + init(); + } + + private void init() + { + events = new CarpetEventServer(this); + modules = new HashMap<>(); + unloadableModules = new HashSet<>(); + tickStart = 0L; + stopAll = false; + holyMoly = server.getCommands().getDispatcher().getRoot().getChildren().stream().map(CommandNode::getName).collect(Collectors.toSet()); + globalHost = CarpetScriptHost.create(this, null, false, null, p -> true, false, null); + } + + public void initializeForWorld() + { + if (Vanilla.MinecraftServer_doScriptsAutoload(server)) + { + for (String moduleName : listAvailableModules(false)) + { + addScriptHost(server.createCommandSourceStack(), moduleName, null, true, true, false, null); + } + } + CarpetEventServer.Event.START.onTick(server); + } + + public Module getModule(String name, boolean allowLibraries) + { + try + { + Path folder = server.getWorldPath(LevelResource.ROOT).resolve("scripts"); + if (!Files.exists(folder)) + { + Files.createDirectories(folder); + } + try (Stream folderLister = Files.list(folder)) + { + Optional scriptPath = folderLister + .filter(script -> + script.getFileName().toString().equalsIgnoreCase(name + ".sc") || + (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl")) + ).findFirst(); + if (scriptPath.isPresent()) + { + return Module.fromPath(scriptPath.get()); + } + } + + Module globalModule = Carpet.fetchGlobalModule(name, allowLibraries); + if (globalModule != null) + { + return globalModule; + } + } + catch (IOException e) + { + LOG.error("Exception while loading the app: ", e); + } + for (Module moduleData : bundledModuleData) + { + if (moduleData.name().equalsIgnoreCase(name) && (allowLibraries || !moduleData.library())) + { + return moduleData; + } + } + return null; + } + + public Module getRuleModule(String name) + { + for (Module moduleData : ruleModuleData) + { + if (moduleData.name().equalsIgnoreCase(name)) + { + return moduleData; + } + } + return null; + } + + public List listAvailableModules(boolean includeBuiltIns) + { + List moduleNames = new ArrayList<>(); + if (includeBuiltIns) + { + for (Module mi : bundledModuleData) + { + if (!mi.library() && !mi.name().endsWith("_beta")) + { + moduleNames.add(mi.name()); + } + } + } + try + { + Path worldScripts = server.getWorldPath(LevelResource.ROOT).resolve("scripts"); + if (!Files.exists(worldScripts)) + { + Files.createDirectories(worldScripts); + } + try (Stream folderLister = Files.list(worldScripts)) + { + folderLister + .filter(f -> f.toString().endsWith(".sc")) + .forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$", "").toLowerCase(Locale.ROOT))); + } + + Carpet.addGlobalModules(moduleNames, includeBuiltIns); + + } + catch (IOException e) + { + LOG.error("Exception while searching for apps: ", e); + } + return moduleNames; + } + + public CarpetScriptHost getAppHostByName(String name) + { + return name == null ? globalHost : modules.get(name); + } + + public boolean addScriptHost(CommandSourceStack source, String name, @Nullable Predicate commandValidator, + boolean perPlayer, boolean autoload, boolean isRuleApp, AppStoreManager.StoreNode installer) + { + Runnable token = Carpet.startProfilerSection("Scarpet load"); + if (commandValidator == null) + { + commandValidator = p -> true; + } + long start = System.nanoTime(); + name = name.toLowerCase(Locale.ROOT); + boolean reload = false; + if (modules.containsKey(name)) + { + if (isRuleApp) + { + return false; + } + removeScriptHost(source, name, false, isRuleApp); + reload = true; + } + Module module = isRuleApp ? getRuleModule(name) : getModule(name, false); + if (module == null) + { + Carpet.Messenger_message(source, "r Failed to add " + name + " app: App not found"); + return false; + } + CarpetScriptHost newHost; + try + { + newHost = CarpetScriptHost.create(this, module, perPlayer, source, commandValidator, isRuleApp, installer); + } + catch (LoadException e) + { + Carpet.Messenger_message(source, "r Failed to add " + name + " app" + (e.getMessage() == null ? "" : ": " + e.getMessage())); + return false; + } + + modules.put(name, newHost); + if (!isRuleApp) + { + unloadableModules.add(name); + } + + if (autoload && !newHost.persistenceRequired) + { + removeScriptHost(source, name, false, false); + return false; + } + String action = (installer != null) ? (reload ? "reinstalled" : "installed") : (reload ? "reloaded" : "loaded"); + + String finalName = name; + Boolean isCommandAdded = newHost.addAppCommands(s -> { + if (!isRuleApp) + { + Carpet.Messenger_message(source, "r Failed to add app '" + finalName + "': ", s); + } + }); + if (isCommandAdded == null) // error should be dispatched + { + removeScriptHost(source, name, false, isRuleApp); + return false; + } + else if (isCommandAdded) + { + Vanilla.MinecraftServer_notifyPlayersCommandsChanged(server); + if (!isRuleApp) + { + Carpet.Messenger_message(source, "gi " + name + " app " + action + " with /" + name + " command"); + } + } + else + { + if (!isRuleApp) + { + Carpet.Messenger_message(source, "gi " + name + " app " + action); + } + } + + if (newHost.isPerUser()) + { + // that will provide player hosts right at the startup + for (ServerPlayer player : source.getServer().getPlayerList().getPlayers()) + { + newHost.retrieveForExecution(player.createCommandSourceStack(), player); + } + } + else + { + // global app - calling start now. + FunctionValue onStart = newHost.getFunction("__on_start"); + if (onStart != null) + { + newHost.callNow(onStart, Collections.emptyList()); + } + } + token.run(); + long end = System.nanoTime(); + LOG.info("App " + name + " loaded in " + (end - start) / 1000000 + " ms"); + return true; + } + + public boolean isInvalidCommandRoot(String appName) + { + return holyMoly.contains(appName); + } + + + public boolean removeScriptHost(CommandSourceStack source, String name, boolean notifySource, boolean isRuleApp) + { + name = name.toLowerCase(Locale.ROOT); + if (!modules.containsKey(name) || (!isRuleApp && !unloadableModules.contains(name))) + { + if (notifySource) + { + Carpet.Messenger_message(source, "r No such app found: ", "wb " + name); + } + return false; + } + // stop all events associated with name + CarpetScriptHost host = modules.remove(name); + events.removeAllHostEvents(host); + host.onClose(); + if (host.hasCommand) + { + Vanilla.CommandDispatcher_unregisterCommand(server.getCommands().getDispatcher(), name); + } + if (!isRuleApp) + { + unloadableModules.remove(name); + } + Vanilla.MinecraftServer_notifyPlayersCommandsChanged(server); + if (notifySource) + { + Carpet.Messenger_message(source, "gi Removed " + name + " app"); + } + return true; + } + + public boolean uninstallApp(CommandSourceStack source, String name) + { + try + { + name = name.toLowerCase(Locale.ROOT); + Path folder = server.getWorldPath(LevelResource.ROOT).resolve("scripts/trash"); + if (!Files.exists(folder)) + { + Files.createDirectories(folder); + } + if (!Files.exists(folder.getParent().resolve(name + ".sc"))) + { + Carpet.Messenger_message(source, "App doesn't exist in the world scripts folder, so can only be unloaded"); + return false; + } + removeScriptHost(source, name, false, false); + Files.move(folder.getParent().resolve(name + ".sc"), folder.resolve(name + ".sc"), StandardCopyOption.REPLACE_EXISTING); + Carpet.Messenger_message(source, "gi Removed " + name + " app"); + return true; + } + catch (IOException exc) + { + Carpet.Messenger_message(source, "rb Failed to uninstall the app"); + } + return false; + } + + public void tick() + { + Runnable token; + token = Carpet.startProfilerSection("Scarpet schedule"); + events.handleEvents.getWhileDisabled(() -> { + events.tick(); + return null; + }); + token.run(); + token = Carpet.startProfilerSection("Scarpet app data"); + for (CarpetScriptHost host : modules.values()) + { + host.tick(); + } + token.run(); + } + + public void onClose() + { + CarpetEventServer.Event.SHUTDOWN.onTick(server); + for (CarpetScriptHost host : modules.values()) + { + host.onClose(); + events.removeAllHostEvents(host); + } + stopAll = true; + } + + public void onPlayerJoin(ServerPlayer player) + { + PLAYER_CONNECTS.onPlayerEvent(player); + modules.values().forEach(h -> + { + if (h.isPerUser()) + { + try + { + h.retrieveOwnForExecution(player.createCommandSourceStack()); + } + catch (CommandSyntaxException ignored) + { + } + } + }); + } + + @Override + public Path resolveResource(String suffix) + { + return server.getWorldPath(LevelResource.ROOT).resolve("scripts/" + suffix); + } + + public void onPlayerLoggedOut(ServerPlayer player, Component reason) + { + if (PLAYER_DISCONNECTS.isNeeded()) + { + PLAYER_DISCONNECTS.onPlayerMessage(player, reason.getContents().toString()); + } + } + + private record TransferData(boolean perUser, Predicate commandValidator, + boolean isRuleApp) + { + private TransferData(CarpetScriptHost host) + { + this(host.perUser, host.commandValidator, host.isRuleApp); + } + } + + public void reload(MinecraftServer server) + { + Map apps = new HashMap<>(); + modules.forEach((s, h) -> apps.put(s, new TransferData(h))); + apps.keySet().forEach(s -> removeScriptHost(server.createCommandSourceStack(), s, false, false)); + CarpetEventServer.Event.clearAllBuiltinEvents(); + init(); + apps.forEach((s, data) -> addScriptHost(server.createCommandSourceStack(), s, data.commandValidator, data.perUser, false, data.isRuleApp, null)); + } + + public void reAddCommands() + { + modules.values().forEach(host -> host.addAppCommands(s -> { + })); + } + + private static boolean bootstrapDone = false; + public static void parseFunctionClasses() + { + if (bootstrapDone) return; + bootstrapDone = true; + ExpressionException.prepareForDoom(); // see fc-#1172 + // Language + AnnotationParser.parseFunctionClass(Arithmetic.class); + AnnotationParser.parseFunctionClass(ControlFlow.class); + AnnotationParser.parseFunctionClass(DataStructures.class); + AnnotationParser.parseFunctionClass(Functions.class); + AnnotationParser.parseFunctionClass(Loops.class); + AnnotationParser.parseFunctionClass(Sys.class); + AnnotationParser.parseFunctionClass(Threading.class); + + // API + AnnotationParser.parseFunctionClass(Auxiliary.class); + AnnotationParser.parseFunctionClass(BlockIterators.class); + AnnotationParser.parseFunctionClass(Entities.class); + AnnotationParser.parseFunctionClass(Inventories.class); + AnnotationParser.parseFunctionClass(Scoreboards.class); + AnnotationParser.parseFunctionClass(carpet.script.language.Threading.class); + AnnotationParser.parseFunctionClass(WorldAccess.class); + } +} diff --git a/src/main/java/carpet/script/Context.java b/src/main/java/carpet/script/Context.java new file mode 100644 index 0000000..01c761c --- /dev/null +++ b/src/main/java/carpet/script/Context.java @@ -0,0 +1,200 @@ +package carpet.script; + +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.ThreadValue; +import carpet.script.value.Value; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Context +{ + public enum Type + { + NONE, VOID, BOOLEAN, NUMBER, STRING, LIST, ITERATOR, SIGNATURE, LOCALIZATION, LVALUE, MAPDEF + } + + public static final Type NONE = Type.NONE; + public static final Type VOID = Type.VOID; + public static final Type BOOLEAN = Type.BOOLEAN; + public static final Type NUMBER = Type.NUMBER; + public static final Type STRING = Type.STRING; + public static final Type LIST = Type.LIST; + public static final Type ITERATOR = Type.ITERATOR; + public static final Type SIGNATURE = Type.SIGNATURE; + public static final Type LOCALIZATION = Type.LOCALIZATION; + public static final Type LVALUE = Type.LVALUE; + public static final Type MAPDEF = Type.MAPDEF; + + public Map variables = new HashMap<>(); + + public final ScriptHost host; + + private ThreadValue threadContext = null; + + public Context(ScriptHost host) + { + this.host = host; + } + + public LazyValue getVariable(String name) + { + return variables.get(name); + } + + public void setVariable(String name, LazyValue lv) + { + variables.put(name, lv); + } + + public void delVariable(String variable) + { + variables.remove(variable); + } + + public void removeVariablesMatching(String varname) + { + variables.entrySet().removeIf(e -> e.getKey().startsWith(varname)); + } + + public Context with(String variable, LazyValue lv) + { + variables.put(variable, lv); + return this; + } + + public Set getAllVariableNames() + { + return variables.keySet(); + } + + public Context recreate() + { + Context ctx = duplicate(); + ctx.threadContext = threadContext; + ctx.initialize(); + return ctx; + } + + public void setThreadContext(ThreadValue callingThread) + { + this.threadContext = callingThread; + } + + public ThreadValue getThreadContext() + { + return threadContext; + } + + protected void initialize() + { + //special variables for second order functions so we don't need to check them all the time + variables.put("_", (c, t) -> Value.ZERO); + variables.put("_i", (c, t) -> Value.ZERO); + variables.put("_a", (c, t) -> Value.ZERO); + } + + public Context duplicate() + { + return new Context(this.host); + } + + public ScriptHost.ErrorSnooper getErrorSnooper() + { + return host.errorSnooper; + } + + public ScriptServer scriptServer() + { + return host.scriptServer(); + } + + /** + * immutable context only for reason on reporting access violations in evaluating expressions in optimizization + * mode detecting any potential violations that may happen on the way + */ + public static class ContextForErrorReporting extends Context + { + public ScriptHost.ErrorSnooper optmizerEerrorSnooper; + + public ContextForErrorReporting(Context parent) + { + super(null); + optmizerEerrorSnooper = parent.host.errorSnooper; + } + + @Override + public ScriptHost.ErrorSnooper getErrorSnooper() + { + return optmizerEerrorSnooper; + } + + public void badProgrammer() + { + throw new InternalExpressionException("Attempting to access the execution context while optimizing the code;" + + " This is not the problem with your code, but the error cause by improper use of code compile optimizations" + + "of scarpet authors. Please report this issue directly to the scarpet issue tracker"); + + } + + @Override + public LazyValue getVariable(String name) + { + badProgrammer(); + return null; + } + + @Override + public void setVariable(String name, LazyValue lv) + { + badProgrammer(); + } + + @Override + public void delVariable(String variable) + { + badProgrammer(); + } + + @Override + public void removeVariablesMatching(String varname) + { + badProgrammer(); + } + + @Override + public Context with(String variable, LazyValue lv) + { + badProgrammer(); + return this; + } + + @Override + public Set getAllVariableNames() + { + badProgrammer(); + return null; + } + + @Override + public Context recreate() + { + badProgrammer(); + return null; + } + + @Override + protected void initialize() + { + badProgrammer(); + } + + @Override + public Context duplicate() + { + badProgrammer(); + return null; + } + } +} diff --git a/src/main/java/carpet/script/EntityEventsGroup.java b/src/main/java/carpet/script/EntityEventsGroup.java new file mode 100644 index 0000000..7f59f21 --- /dev/null +++ b/src/main/java/carpet/script/EntityEventsGroup.java @@ -0,0 +1,178 @@ +package carpet.script; + +import carpet.script.exception.InternalExpressionException; +import carpet.script.external.Vanilla; +import carpet.script.value.EntityValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +public class EntityEventsGroup +{ + private record EventKey(String host, String user) + { + } + + private final Map> actions; + private final Entity entity; + + public EntityEventsGroup(Entity e) + { + actions = new HashMap<>(); + entity = e; + } + + public void onEvent(Event type, Object... args) + { + if (actions.isEmpty()) + { + return; // most of the cases, trying to be nice + } + Map actionSet = actions.get(type); + if (actionSet == null) + { + return; + } + CarpetScriptServer scriptServer = Vanilla.MinecraftServer_getScriptServer(entity.getServer()); + if (scriptServer.stopAll) + { + return; // executed after world is closin down + } + for (Iterator> iterator = actionSet.entrySet().iterator(); iterator.hasNext(); ) + { + Map.Entry action = iterator.next(); + EventKey key = action.getKey(); + ScriptHost host = scriptServer.getAppHostByName(key.host()); + if (host == null) + { + iterator.remove(); + continue; + } + if (key.user() != null && entity.getServer().getPlayerList().getPlayerByName(key.user()) == null) + { + iterator.remove(); + continue; + } + if (type.call(action.getValue(), entity, args) == CarpetEventServer.CallbackResult.FAIL) + { + iterator.remove(); + } + } + if (actionSet.isEmpty()) + { + actions.remove(type); + } + } + + public void addEvent(Event type, ScriptHost host, FunctionValue fun, List extraargs) + { + EventKey key = new EventKey(host.getName(), host.user); + if (fun != null) + { + CarpetEventServer.Callback call = type.create(key, fun, extraargs, (CarpetScriptServer) host.scriptServer()); + if (call == null) + { + throw new InternalExpressionException("wrong number of arguments for callback, required " + type.argcount); + } + actions.computeIfAbsent(type, k -> new HashMap<>()).put(key, call); + } + else + { + actions.computeIfAbsent(type, k -> new HashMap<>()).remove(key); + if (actions.get(type).isEmpty()) + { + actions.remove(type); + } + } + } + + + public static class Event + { + public static final Map byName = new HashMap<>(); + public static final Event ON_DEATH = new Event("on_death", 1) + { + @Override + public List makeArgs(Entity entity, Object... providedArgs) + { + return Arrays.asList( + new EntityValue(entity), + new StringValue((String) providedArgs[0]) + ); + } + }; + public static final Event ON_REMOVED = new Event("on_removed", 0); + public static final Event ON_TICK = new Event("on_tick", 0); + public static final Event ON_DAMAGE = new Event("on_damaged", 3) + { + @Override + public List makeArgs(Entity entity, Object... providedArgs) + { + float amount = (Float) providedArgs[0]; + DamageSource source = (DamageSource) providedArgs[1]; + return Arrays.asList( + new EntityValue(entity), + new NumericValue(amount), + new StringValue(source.getMsgId()), + source.getEntity() == null ? Value.NULL : new EntityValue(source.getEntity()) + ); + } + }; + public static final Event ON_MOVE = new Event("on_move", 3) + { + @Override + public List makeArgs(Entity entity, Object... providedArgs) + { + return Arrays.asList( + new EntityValue(entity), + ValueConversions.of((Vec3) providedArgs[0]), + ValueConversions.of((Vec3) providedArgs[1]), + ValueConversions.of((Vec3) providedArgs[2]) + ); + } + }; + + public final int argcount; + public final String id; + + public Event(String identifier, int args) + { + id = identifier; + argcount = args + 1; // entity is not extra + byName.put(identifier, this); + } + + public CarpetEventServer.Callback create(EventKey key, FunctionValue function, List extraArgs, CarpetScriptServer scriptServer) + { + if ((function.getArguments().size() - (extraArgs == null ? 0 : extraArgs.size())) != argcount) + { + return null; + } + return new CarpetEventServer.Callback(key.host(), key.user(), function, extraArgs, scriptServer); + } + + public CarpetEventServer.CallbackResult call(CarpetEventServer.Callback tickCall, Entity entity, Object... args) + { + assert args.length == argcount - 1; + return tickCall.execute(entity.createCommandSourceStack(), makeArgs(entity, args)); + } + + protected List makeArgs(Entity entity, Object... args) + { + return Collections.singletonList(new EntityValue(entity)); + } + } +} diff --git a/src/main/java/carpet/script/Expression.java b/src/main/java/carpet/script/Expression.java new file mode 100644 index 0000000..7f427a4 --- /dev/null +++ b/src/main/java/carpet/script/Expression.java @@ -0,0 +1,1599 @@ +package carpet.script; + +import carpet.script.Fluff.AbstractFunction; +import carpet.script.Fluff.AbstractLazyFunction; +import carpet.script.Fluff.AbstractLazyOperator; +import carpet.script.Fluff.AbstractOperator; +import carpet.script.Fluff.AbstractUnaryOperator; +import carpet.script.Fluff.ILazyFunction; +import carpet.script.Fluff.ILazyOperator; +import carpet.script.Fluff.QuadFunction; +import carpet.script.Fluff.QuinnFunction; +import carpet.script.Fluff.SexFunction; +import carpet.script.Fluff.TriFunction; +import carpet.script.exception.BreakStatement; +import carpet.script.exception.ContinueStatement; +import carpet.script.exception.ExitStatement; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.IntegrityException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ResolvedException; +import carpet.script.exception.ReturnStatement; +import carpet.script.external.Vanilla; +import carpet.script.language.Arithmetic; +import carpet.script.language.ControlFlow; +import carpet.script.language.DataStructures; +import carpet.script.language.Functions; +import carpet.script.language.Loops; +import carpet.script.language.Operators; +import carpet.script.language.Sys; +import carpet.script.language.Threading; +import carpet.script.value.FunctionValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import it.unimi.dsi.fastutil.Stack; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleToLongFunction; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.function.LongBinaryOperator; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class Expression +{ + /** + * The current infix expression + */ + private String expression; + + String getCodeString() + { + return expression; + } + + private boolean allowNewlineSubstitutions = true; + private boolean allowComments = false; + + public Module module = null; + + public String getModuleName() + { + return module == null ? "system chat" : module.name(); + } + + public void asATextSource() + { + allowNewlineSubstitutions = false; + allowComments = true; + } + + public void asAModule(Module mi) + { + module = mi; + } + + /** + * Cached AST (Abstract Syntax Tree) (root) of the expression + */ + private LazyValue ast = null; + + /** + * script specific operatos and built-in functions + */ + private final Map operators = new Object2ObjectOpenHashMap<>(); + + public boolean isAnOperator(String opname) + { + return operators.containsKey(opname) || operators.containsKey(opname + "u"); + } + + private final Map functions = new Object2ObjectOpenHashMap<>(); + + public Set getFunctionNames() + { + return functions.keySet(); + } + + private final Map functionalEquivalence = new Object2ObjectOpenHashMap<>(); + + public void addFunctionalEquivalence(String operator, String function) + { + assert operators.containsKey(operator); + assert functions.containsKey(function); + functionalEquivalence.put(operator, function); + } + + private final Map constants = Map.of( + "euler", Arithmetic.euler, + "pi", Arithmetic.PI, + "null", Value.NULL, + "true", Value.TRUE, + "false", Value.FALSE + ); + + protected Value getConstantFor(String surface) + { + return constants.get(surface); + } + + public List getExpressionSnippet(Tokenizer.Token token) + { + String code = this.getCodeString(); + List output = new ArrayList<>(getExpressionSnippetLeftContext(token, code, 1)); + List context = getExpressionSnippetContext(token, code); + output.add(context.get(0) + " HERE>> " + context.get(1)); + output.addAll(getExpressionSnippetRightContext(token, code, 1)); + return output; + } + + private static List getExpressionSnippetLeftContext(Tokenizer.Token token, String expr, int contextsize) + { + List output = new ArrayList<>(); + String[] lines = expr.split("\n"); + if (lines.length == 1) + { + return output; + } + for (int lno = token.lineno - 1; lno >= 0 && output.size() < contextsize; lno--) + { + output.add(lines[lno]); + } + Collections.reverse(output); + return output; + } + + private static List getExpressionSnippetContext(Tokenizer.Token token, String expr) + { + List output = new ArrayList<>(); + String[] lines = expr.split("\n"); + if (lines.length > 1) + { + output.add(lines[token.lineno].substring(0, token.linepos)); + output.add(lines[token.lineno].substring(token.linepos)); + } + else + { + output.add(expr.substring(Math.max(0, token.pos - 40), token.pos)); + output.add(expr.substring(token.pos, Math.min(token.pos + 1 + 40, expr.length()))); + } + return output; + } + + private static List getExpressionSnippetRightContext(Tokenizer.Token token, String expr, int contextsize) + { + List output = new ArrayList<>(); + String[] lines = expr.split("\n"); + if (lines.length == 1) + { + return output; + } + for (int lno = token.lineno + 1; lno < lines.length && output.size() < contextsize; lno++) + { + output.add(lines[lno]); + } + return output; + } + + + public void addLazyUnaryOperator(String surface, int precedence, boolean leftAssoc, boolean pure, Function staticTyper, + TriFunction lazyfun) + { + operators.put(surface + "u", new AbstractLazyOperator(precedence, leftAssoc) + { + @Override + public boolean pure() + { + return pure; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public Context.Type staticType(Context.Type outerType) + { + return staticTyper.apply(outerType); + } + + @Override + public LazyValue lazyEval(Context c, Context.Type t, Expression e, Tokenizer.Token token, LazyValue v, LazyValue v2) + { + try + { + return lazyfun.apply(c, t, v); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, token); + } + } + }); + } + + + public void addLazyBinaryOperatorWithDelegation(String surface, int precedence, boolean leftAssoc, boolean pure, + SexFunction lazyfun) + { + operators.put(surface, new AbstractLazyOperator(precedence, leftAssoc) + { + @Override + public boolean pure() + { + return pure; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type type, Expression e, Tokenizer.Token t, LazyValue v1, LazyValue v2) + { + try + { + return lazyfun.apply(c, type, e, t, v1, v2); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addCustomFunction(String name, ILazyFunction fun) + { + functions.put(name, fun); + } + + public void addLazyFunctionWithDelegation(String name, int numpar, boolean pure, boolean transitive, + QuinnFunction, LazyValue> lazyfun) + { + functions.put(name, new AbstractLazyFunction(numpar, name) + { + @Override + public boolean pure() + { + return pure; + } + + @Override + public boolean transitive() + { + return transitive; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type type, Expression e, Tokenizer.Token t, List lv) + { + ILazyFunction.checkInterrupts(); + try + { + return lazyfun.apply(c, type, e, t, lv); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addFunctionWithDelegation(String name, int numpar, boolean pure, boolean transitive, + QuinnFunction, Value> fun) + { + functions.put(name, new AbstractLazyFunction(numpar, name) + { + @Override + public boolean pure() + { + return pure; + } + + @Override + public boolean transitive() + { + return transitive; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type type, Expression e, Tokenizer.Token t, List lv) + { + try + { + Value res = fun.apply(c, type, e, t, unpackArgs(lv, c, Context.NONE)); + return (cc, tt) -> res; + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addLazyBinaryOperator(String surface, int precedence, boolean leftAssoc, boolean pure, Function typer, + QuadFunction lazyfun) + { + operators.put(surface, new AbstractLazyOperator(precedence, leftAssoc) + { + + @Override + public boolean pure() + { + return pure; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public Context.Type staticType(Context.Type outerType) + { + return typer.apply(outerType); + } + + @Override + public LazyValue lazyEval(Context c, Context.Type t, Expression e, Tokenizer.Token token, LazyValue v1, LazyValue v2) + { + ILazyFunction.checkInterrupts(); + try + { + return lazyfun.apply(c, t, v1, v2); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, token); + } + } + }); + } + + public void addBinaryContextOperator(String surface, int precedence, boolean leftAssoc, boolean pure, boolean transitive, + QuadFunction fun) + { + operators.put(surface, new AbstractLazyOperator(precedence, leftAssoc) + { + @Override + public boolean pure() + { + return pure; + } + + @Override + public boolean transitive() + { + return transitive; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type t, Expression e, Tokenizer.Token token, LazyValue v1, LazyValue v2) + { + try + { + Value ret = fun.apply(c, t, v1.evalValue(c, Context.NONE), v2.evalValue(c, Context.NONE)); + return (cc, tt) -> ret; + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, token); + } + } + }); + } + + public static RuntimeException handleCodeException(Context c, RuntimeException exc, Expression e, Tokenizer.Token token) + { + if (exc instanceof ExitStatement) + { + return exc; + } + if (exc instanceof IntegrityException) + { + return exc; + } + if (exc instanceof final InternalExpressionException iee) + { + return iee.promote(c, e, token); + } + if (exc instanceof ArithmeticException) + { + return new ExpressionException(c, e, token, "Your math is wrong, " + exc.getMessage()); + } + if (exc instanceof ResolvedException) + { + return exc; + } + // unexpected really - should be caught earlier and converted to InternalExpressionException + CarpetScriptServer.LOG.error("Unexpected exception while running Scarpet code", exc); + return new ExpressionException(c, e, token, "Internal error (please report this issue to Carpet) while evaluating: " + exc); + } + + public void addUnaryOperator(String surface, boolean leftAssoc, Function fun) + { + operators.put(surface + "u", new AbstractUnaryOperator(Operators.precedence.get("unary+-!..."), leftAssoc) + { + @Override + public Value evalUnary(Value v1) + { + return fun.apply(v1); + } + }); + } + + public void addBinaryOperator(String surface, int precedence, boolean leftAssoc, BiFunction fun) + { + operators.put(surface, new AbstractOperator(precedence, leftAssoc) + { + @Override + public Value eval(Value v1, Value v2) + { + return fun.apply(v1, v2); + } + }); + } + + + public void addUnaryFunction(String name, Function fun) + { + functions.put(name, new AbstractFunction(1, name) + { + @Override + public Value eval(List parameters) + { + return fun.apply(parameters.get(0)); + } + }); + } + + public void addImpureUnaryFunction(String name, Function fun) + { + functions.put(name, new AbstractFunction(1, name) + { + @Override + public boolean pure() + { + return false; + } + + @Override + public Value eval(List parameters) + { + return fun.apply(parameters.get(0)); + } + }); + } + + public void addBinaryFunction(String name, BiFunction fun) + { + functions.put(name, new AbstractFunction(2, name) + { + @Override + public Value eval(List parameters) + { + return fun.apply(parameters.get(0), parameters.get(1)); + } + }); + } + + public void addFunction(String name, Function, Value> fun) + { + functions.put(name, new AbstractFunction(-1, name) + { + @Override + public Value eval(List parameters) + { + return fun.apply(parameters); + } + }); + } + + public void addImpureFunction(String name, Function, Value> fun) + { + functions.put(name, new AbstractFunction(-1, name) + { + @Override + public boolean pure() + { + return false; + } + + @Override + public Value eval(List parameters) + { + return fun.apply(parameters); + } + }); + } + + public void addMathematicalUnaryFunction(String name, DoubleUnaryOperator fun) + { + addUnaryFunction(name, (v) -> new NumericValue(fun.applyAsDouble(NumericValue.asNumber(v).getDouble()))); + } + + public void addMathematicalUnaryIntFunction(String name, DoubleToLongFunction fun) + { + addUnaryFunction(name, (v) -> new NumericValue(fun.applyAsLong(NumericValue.asNumber(v).getDouble()))); + } + + public void addMathematicalBinaryIntFunction(String name, LongBinaryOperator fun) + { + addBinaryFunction(name, (w, v) -> + new NumericValue(fun.applyAsLong(NumericValue.asNumber(w).getLong(), NumericValue.asNumber(v).getLong()))); + } + + public void addMathematicalBinaryFunction(String name, DoubleBinaryOperator fun) + { + addBinaryFunction(name, (w, v) -> + new NumericValue(fun.applyAsDouble(NumericValue.asNumber(w).getDouble(), NumericValue.asNumber(v).getDouble()))); + } + + + public void addLazyFunction(String name, int numParams, TriFunction, LazyValue> fun) + { + functions.put(name, new AbstractLazyFunction(numParams, name) + { + @Override + public boolean pure() + { + return false; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type i, Expression e, Tokenizer.Token t, List lazyParams) + { + ILazyFunction.checkInterrupts(); + if (numParams >= 0 && lazyParams.size() != numParams) + { + String error = "Function '" + name + "' requires " + numParams + " arguments, got " + lazyParams.size() + ". "; + throw new InternalExpressionException(error + (fun instanceof Fluff.UsageProvider up ? up.getUsage() : "")); + } + + try + { + return fun.apply(c, i, lazyParams); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addLazyFunction(String name, TriFunction, LazyValue> fun) + { + functions.put(name, new AbstractLazyFunction(-1, name) + { + @Override + public boolean pure() + { + return false; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type i, Expression e, Tokenizer.Token t, List lazyParams) + { + ILazyFunction.checkInterrupts(); + try + { + return fun.apply(c, i, lazyParams); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addPureLazyFunction(String name, int num_params, Function typer, TriFunction, LazyValue> fun) + { + functions.put(name, new AbstractLazyFunction(num_params, name) + { + @Override + public boolean pure() + { + return true; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public Context.Type staticType(Context.Type outerType) + { + return typer.apply(outerType); + } + + @Override + public LazyValue lazyEval(Context c, Context.Type i, Expression e, Tokenizer.Token t, List lazyParams) + { + ILazyFunction.checkInterrupts(); + try + { + return fun.apply(c, i, lazyParams); + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addContextFunction(String name, int num_params, TriFunction, Value> fun) + { + functions.put(name, new AbstractLazyFunction(num_params, name) + { + @Override + public boolean pure() + { + return false; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type i, Expression e, Tokenizer.Token t, List lazyParams) + { + ILazyFunction.checkInterrupts(); + try + { + Value ret = fun.apply(c, i, unpackArgs(lazyParams, c, Context.NONE)); + return (cc, tt) -> ret; + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public void addTypedContextFunction(String name, int num_params, Context.Type reqType, TriFunction, Value> fun) + { + functions.put(name, new AbstractLazyFunction(num_params, name) + { + @Override + public boolean pure() + { + return true; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public Context.Type staticType(Context.Type outerType) + { + return reqType; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type i, Expression e, Tokenizer.Token t, List lazyParams) + { + try + { + Value ret = fun.apply(c, i, unpackArgs(lazyParams, c, reqType)); + return (cc, tt) -> ret; + } + catch (RuntimeException exc) + { + throw handleCodeException(c, exc, e, t); + } + } + }); + } + + public FunctionValue createUserDefinedFunction(Context context, String name, Expression expr, Tokenizer.Token token, List arguments, String varArgs, List outers, LazyValue code) + { + if (functions.containsKey(name)) + { + throw new ExpressionException(context, expr, token, "Function " + name + " would mask a built-in function"); + } + Map contextValues = new HashMap<>(); + for (String outer : outers) + { + LazyValue lv = context.getVariable(outer); + if (lv == null) + { + throw new InternalExpressionException("Variable " + outer + " needs to be defined in outer scope to be used as outer parameter, and cannot be global"); + } + else + { + contextValues.put(outer, lv); + } + } + if (contextValues.isEmpty()) + { + contextValues = null; + } + + FunctionValue result = new FunctionValue(expr, token, name, code, arguments, varArgs, contextValues); + // do not store lambda definitions + if (!name.equals("_")) + { + context.host.addUserDefinedFunction(context, module, name, result); + } + return result; + } + + public void alias(String copy, String original) + { + ILazyFunction originalFunction = functions.get(original); + functions.put(copy, new ILazyFunction() + { + @Override + public int getNumParams() + { + return originalFunction.getNumParams(); + } + + @Override + public boolean numParamsVaries() + { + return originalFunction.numParamsVaries(); + } + + @Override + public boolean pure() + { + return originalFunction.pure(); + } + + @Override + public boolean transitive() + { + return originalFunction.transitive(); + } + + @Override + public Context.Type staticType(Context.Type outerType) + { + return originalFunction.staticType(outerType); + } + + @Override + public LazyValue lazyEval(Context c, Context.Type type, Expression expr, Tokenizer.Token token, List lazyParams) + { + c.host.issueDeprecation(copy + "(...)"); + return originalFunction.lazyEval(c, type, expr, token, lazyParams); + } + }); + } + + + public void setAnyVariable(Context c, String name, LazyValue lv) + { + if (name.startsWith("global_")) + { + c.host.setGlobalVariable(module, name, lv); + } + else + { + c.setVariable(name, lv); + } + } + + public LazyValue getOrSetAnyVariable(Context c, String name) + { + LazyValue variable; + if (!name.startsWith("global_")) + { + variable = c.getVariable(name); + if (variable != null) + { + return variable; + } + } + variable = c.host.getGlobalVariable(module, name); + if (variable != null) + { + return variable; + } + variable = (_c, _t) -> _c.host.strict ? Value.UNDEF.reboundedTo(name) : Value.NULL.reboundedTo(name); + setAnyVariable(c, name, variable); + return variable; + } + + public static final Expression none = new Expression("null"); + + /** + * @param expression . + */ + public Expression(String expression) + { + this.expression = expression.stripTrailing().replaceAll("\\r\\n?", "\n").replaceAll("\\t", " "); + Operators.apply(this); + ControlFlow.apply(this); + Functions.apply(this); + Arithmetic.apply(this); + Sys.apply(this); + Threading.apply(this); + Loops.apply(this); + DataStructures.apply(this); + } + + + private List shuntingYard(Context c) + { + List outputQueue = new ArrayList<>(); + Stack stack = new ObjectArrayList<>(); + + Tokenizer tokenizer = new Tokenizer(c, this, expression, allowComments, allowNewlineSubstitutions); + // stripping lousy but acceptable semicolons + List cleanedTokens = tokenizer.postProcess(); + + Tokenizer.Token lastFunction = null; + Tokenizer.Token previousToken = null; + for (Tokenizer.Token token : cleanedTokens) + { + switch (token.type) + { + case STRINGPARAM: + //stack.push(token); // changed that so strings are treated like literals + //break; + case LITERAL, HEX_LITERAL: + if (previousToken != null && ( + previousToken.type == Tokenizer.Token.TokenType.LITERAL || + previousToken.type == Tokenizer.Token.TokenType.HEX_LITERAL || + previousToken.type == Tokenizer.Token.TokenType.STRINGPARAM)) + { + throw new ExpressionException(c, this, token, "Missing operator"); + } + outputQueue.add(token); + break; + case VARIABLE: + outputQueue.add(token); + break; + case FUNCTION: + stack.push(token); + lastFunction = token; + break; + case COMMA: + if (previousToken != null && previousToken.type == Tokenizer.Token.TokenType.OPERATOR) + { + throw new ExpressionException(c, this, previousToken, "Missing parameter(s) for operator "); + } + while (!stack.isEmpty() && stack.top().type != Tokenizer.Token.TokenType.OPEN_PAREN) + { + outputQueue.add(stack.pop()); + } + if (stack.isEmpty()) + { + if (lastFunction == null) + { + throw new ExpressionException(c, this, token, "Unexpected comma"); + } + else + { + throw new ExpressionException(c, this, lastFunction, "Parse error for function"); + } + } + break; + case OPERATOR: + { + if (previousToken != null + && (previousToken.type == Tokenizer.Token.TokenType.COMMA || previousToken.type == Tokenizer.Token.TokenType.OPEN_PAREN)) + { + throw new ExpressionException(c, this, token, "Missing parameter(s) for operator '" + token + "'"); + } + ILazyOperator o1 = operators.get(token.surface); + if (o1 == null) + { + throw new ExpressionException(c, this, token, "Unknown operator '" + token + "'"); + } + + shuntOperators(outputQueue, stack, o1); + stack.push(token); + break; + } + case UNARY_OPERATOR: + { + if (previousToken != null && previousToken.type != Tokenizer.Token.TokenType.OPERATOR + && previousToken.type != Tokenizer.Token.TokenType.COMMA && previousToken.type != Tokenizer.Token.TokenType.OPEN_PAREN) + { + throw new ExpressionException(c, this, token, "Invalid position for unary operator " + token); + } + ILazyOperator o1 = operators.get(token.surface); + if (o1 == null) + { + throw new ExpressionException(c, this, token, "Unknown unary operator '" + token.surface.substring(0, token.surface.length() - 1) + "'"); + } + + shuntOperators(outputQueue, stack, o1); + stack.push(token); + break; + } + case OPEN_PAREN: + // removed implicit multiplication in this missing code block + if (previousToken != null && previousToken.type == Tokenizer.Token.TokenType.FUNCTION) + { + outputQueue.add(token); + } + stack.push(token); + break; + case CLOSE_PAREN: + if (previousToken != null && previousToken.type == Tokenizer.Token.TokenType.OPERATOR) + { + throw new ExpressionException(c, this, previousToken, "Missing parameter(s) for operator " + previousToken); + } + while (!stack.isEmpty() && stack.top().type != Tokenizer.Token.TokenType.OPEN_PAREN) + { + outputQueue.add(stack.pop()); + } + if (stack.isEmpty()) + { + throw new ExpressionException(c, this, "Mismatched parentheses"); + } + stack.pop(); + if (!stack.isEmpty() && stack.top().type == Tokenizer.Token.TokenType.FUNCTION) + { + outputQueue.add(stack.pop()); + } + break; + case MARKER: + if ("$".equals(token.surface)) + { + StringBuilder sb = new StringBuilder(expression); + sb.setCharAt(token.pos, '\n'); + expression = sb.toString(); + } + break; + } + if (token.type != Tokenizer.Token.TokenType.MARKER) + { + previousToken = token; + } + } + + while (!stack.isEmpty()) + { + Tokenizer.Token element = stack.pop(); + if (element.type == Tokenizer.Token.TokenType.OPEN_PAREN || element.type == Tokenizer.Token.TokenType.CLOSE_PAREN) + { + throw new ExpressionException(c, this, element, "Mismatched parentheses"); + } + outputQueue.add(element); + } + return outputQueue; + } + + private void shuntOperators(List outputQueue, Stack stack, ILazyOperator o1) + { + Tokenizer.Token nextToken = stack.isEmpty() ? null : stack.top(); + while (nextToken != null + && (nextToken.type == Tokenizer.Token.TokenType.OPERATOR + || nextToken.type == Tokenizer.Token.TokenType.UNARY_OPERATOR) + && ((o1.isLeftAssoc() && o1.getPrecedence() <= operators.get(nextToken.surface).getPrecedence()) + || (o1.getPrecedence() < operators.get(nextToken.surface).getPrecedence()))) + { + outputQueue.add(stack.pop()); + nextToken = stack.isEmpty() ? null : stack.top(); + } + } + + public Value eval(Context c) + { + if (ast == null) + { + ast = getAST(c); + } + return evalValue(() -> ast, c, Context.Type.NONE); + } + + public Value evalValue(Supplier exprProvider, Context c, Context.Type expectedType) + { + try + { + return exprProvider.get().evalValue(c, expectedType); + } + catch (ContinueStatement | BreakStatement | ReturnStatement exc) + { + throw new ExpressionException(c, this, "Control flow functions, like continue, break or return, should only be used in loops, and functions respectively."); + } + catch (ExitStatement exit) + { + return exit.retval == null ? Value.NULL : exit.retval; + } + catch (StackOverflowError ignored) + { + throw new ExpressionException(c, this, "Your thoughts are too deep"); + } + catch (InternalExpressionException exc) + { + throw new ExpressionException(c, this, "Your expression result is incorrect: " + exc.getMessage()); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(c, this, "The final result is incorrect: " + exc.getMessage()); + } + } + + public static class ExpressionNode + { + public LazyValue op; + public List args; + public Tokenizer.Token token; + public List range; + /** + * The Value representation of the left parenthesis, used for parsing + * varying numbers of function parameters. + */ + public static final ExpressionNode PARAMS_START = new ExpressionNode(null, null, Tokenizer.Token.NONE); + + public ExpressionNode(LazyValue op, List args, Tokenizer.Token token) + { + this.op = op; + this.args = args; + this.token = token; + range = new ArrayList<>(); + range.add(token); + } + + public static ExpressionNode ofConstant(Value val, Tokenizer.Token token) + { + return new ExpressionNode(new LazyValue.Constant(val), Collections.emptyList(), token); + } + } + + + private ExpressionNode RPNToParseTree(List tokens, Context context) + { + Stack nodeStack = new ObjectArrayList<>(); + for (Tokenizer.Token token : tokens) + { + switch (token.type) { + case UNARY_OPERATOR -> { + ExpressionNode node = nodeStack.pop(); + LazyValue result = (c, t) -> operators.get(token.surface).lazyEval(c, t, this, token, node.op, null).evalValue(c, t); + nodeStack.push(new ExpressionNode(result, Collections.singletonList(node), token)); + } + case OPERATOR -> { + ExpressionNode v1 = nodeStack.pop(); + ExpressionNode v2 = nodeStack.pop(); + LazyValue result = (c, t) -> operators.get(token.surface).lazyEval(c, t, this, token, v2.op, v1.op).evalValue(c, t); + nodeStack.push(new ExpressionNode(result, List.of(v2, v1), token)); + } + case VARIABLE -> { + Value constant = getConstantFor(token.surface); + if (constant != null) + { + token.morph(Tokenizer.Token.TokenType.CONSTANT, token.surface); + nodeStack.push(new ExpressionNode(LazyValue.ofConstant(constant), Collections.emptyList(), token)); + } + else + { + nodeStack.push(new ExpressionNode(((c, t) -> getOrSetAnyVariable(c, token.surface).evalValue(c, t)), Collections.emptyList(), token)); + } + } + case FUNCTION -> { + String name = token.surface; + ILazyFunction f; + ArrayList p; + boolean isKnown = functions.containsKey(name); // globals will be evaluated lazily, not at compile time via . + if (isKnown) + { + f = functions.get(name); + p = new ArrayList<>(!f.numParamsVaries() ? f.getNumParams() : 0); + } + else // potentially unknown function or just unknown function + { + f = functions.get("call"); + p = new ArrayList<>(); + } + // pop parameters off the stack until we hit the start of + // this function's parameter list + while (!nodeStack.isEmpty() && nodeStack.top() != ExpressionNode.PARAMS_START) + { + p.add(nodeStack.pop()); + } + if (!isKnown) + { + p.add(ExpressionNode.ofConstant(new StringValue(name), token.morphedInto(Tokenizer.Token.TokenType.STRINGPARAM, token.surface))); + token.morph(Tokenizer.Token.TokenType.FUNCTION, "call"); + } + Collections.reverse(p); + if (nodeStack.top() == ExpressionNode.PARAMS_START) + { + nodeStack.pop(); + } + List params = p.stream().map(n -> n.op).collect(Collectors.toList()); + nodeStack.push(new ExpressionNode( + (c, t) -> f.lazyEval(c, t, this, token, params).evalValue(c, t), + p, token + )); + } + case OPEN_PAREN -> nodeStack.push(ExpressionNode.PARAMS_START); + case LITERAL -> { + Value number; + try + { + number = new NumericValue(token.surface); + } + catch (NumberFormatException exception) + { + throw new ExpressionException(context, this, token, "Not a number"); + } + token.morph(Tokenizer.Token.TokenType.CONSTANT, token.surface); + nodeStack.push(ExpressionNode.ofConstant(number, token)); + } + case STRINGPARAM -> { + token.morph(Tokenizer.Token.TokenType.CONSTANT, token.surface); + nodeStack.push(ExpressionNode.ofConstant(new StringValue(token.surface), token)); + } + case HEX_LITERAL -> { + Value hexNumber; + try + { + hexNumber = new NumericValue(new BigInteger(token.surface.substring(2), 16).longValue()); + } + catch (NumberFormatException exception) + { + throw new ExpressionException(context, this, token, "Not a number"); + } + token.morph(Tokenizer.Token.TokenType.CONSTANT, token.surface); + nodeStack.push(ExpressionNode.ofConstant(hexNumber, token)); + } + default -> throw new ExpressionException(context, this, token, "Unexpected token '" + token.surface + "'"); + } + } + return nodeStack.pop(); + } + + private LazyValue getAST(Context context) + { + List rpn = shuntingYard(context); + validate(context, rpn); + ExpressionNode root = RPNToParseTree(rpn, context); + if (!Vanilla.ScriptServer_scriptOptimizations(((CarpetScriptServer)context.scriptServer()).server)) + { + return root.op; + } + + Context optimizeOnlyContext = new Context.ContextForErrorReporting(context); + boolean scriptsDebugging = Vanilla.ScriptServer_scriptDebugging(((CarpetScriptServer)context.scriptServer()).server); + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info("Input code size for " + getModuleName() + ": " + treeSize(root) + " nodes, " + treeDepth(root) + " deep"); + } + + // Defined out here to not need to conditionally assign them with debugging disabled + int prevTreeSize = -1; + int prevTreeDepth = -1; + + boolean changed = true; + while (changed) + { + changed = false; + while (true) + { + if (scriptsDebugging) + { + prevTreeSize = treeSize(root); + prevTreeDepth = treeDepth(root); + } + boolean optimized = compactTree(root, Context.Type.NONE, 0, scriptsDebugging); + if (!optimized) + { + break; + } + changed = true; + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info("Compacted from " + prevTreeSize + " nodes, " + prevTreeDepth + " code depth to " + treeSize(root) + " nodes, " + treeDepth(root) + " code depth"); + } + } + while (true) + { + if (scriptsDebugging) + { + prevTreeSize = treeSize(root); + prevTreeDepth = treeDepth(root); + } + boolean optimized = optimizeTree(optimizeOnlyContext, root, Context.Type.NONE, 0, scriptsDebugging); + if (!optimized) + { + break; + } + changed = true; + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info("Optimized from " + prevTreeSize + " nodes, " + prevTreeDepth + " code depth to " + treeSize(root) + " nodes, " + treeDepth(root) + " code depth"); + } + } + } + return extractOp(optimizeOnlyContext, root, Context.Type.NONE); + } + + private int treeSize(ExpressionNode node) + { + return node.op instanceof LazyValue.ContextFreeLazyValue ? 1 : node.args.stream().mapToInt(this::treeSize).sum() + 1; + } + + private int treeDepth(ExpressionNode node) + { + return node.op instanceof LazyValue.ContextFreeLazyValue ? 1 : node.args.stream().mapToInt(this::treeDepth).max().orElse(0) + 1; + } + + + private boolean compactTree(ExpressionNode node, Context.Type expectedType, int indent, boolean scriptsDebugging) + { + // ctx is just to report errors, not values evaluation + boolean optimized = false; + Tokenizer.Token.TokenType token = node.token.type; + if (!token.isFunctional()) + { + return false; + } + // input special cases here, like function signature + if (node.op instanceof LazyValue.Constant) + { + return false; // optimized already + } + // function or operator + String symbol = node.token.surface; + Fluff.EvalNode operation = ((token == Tokenizer.Token.TokenType.FUNCTION) ? functions : operators).get(symbol); + Context.Type requestedType = operation.staticType(expectedType); + for (ExpressionNode arg : node.args) + { + if (compactTree(arg, requestedType, indent + 1, scriptsDebugging)) + { + optimized = true; + } + } + + if (expectedType != Context.Type.MAPDEF && symbol.equals("->") && node.args.size() == 2) + { + String rop = node.args.get(1).token.surface; + ExpressionNode returnNode = null; + if ((rop.equals(";") || rop.equals("then"))) + { + List thenArgs = node.args.get(1).args; + if (thenArgs.size() > 1 && thenArgs.get(thenArgs.size() - 1).token.surface.equals("return")) + { + returnNode = thenArgs.get(thenArgs.size() - 1); + } + } + else if (rop.equals("return")) + { + returnNode = node.args.get(1); + } + if (returnNode != null) // tail return + { + if (!returnNode.args.isEmpty()) + { + returnNode.op = returnNode.args.get(0).op; + returnNode.token = returnNode.args.get(0).token; + returnNode.range = returnNode.args.get(0).range; + returnNode.args = returnNode.args.get(0).args; + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info(" - Removed unnecessary tail return of " + returnNode.token.surface + " from function body at line " + (returnNode.token.lineno + 1) + ", node depth " + indent); + } + } + else + { + returnNode.op = LazyValue.ofConstant(Value.NULL); + returnNode.token.morph(Tokenizer.Token.TokenType.CONSTANT, ""); + returnNode.args = Collections.emptyList(); + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info(" - Removed unnecessary tail return from function body at line " + (returnNode.token.lineno + 1) + ", node depth " + indent); + } + } + + } + } + for (Map.Entry pair : functionalEquivalence.entrySet()) + { + String operator = pair.getKey(); + String function = pair.getValue(); + if ((symbol.equals(operator) || symbol.equals(function)) && node.args.size() > 0) + { + boolean leftOptimizable = operators.get(operator).isLeftAssoc(); + ExpressionNode optimizedChild = node.args.get(leftOptimizable ? 0 : (node.args.size() - 1)); + String type = optimizedChild.token.surface; + if ((type.equals(operator) || type.equals(function)) && (!(optimizedChild.op instanceof LazyValue.ContextFreeLazyValue))) + { + optimized = true; + List newargs = new ArrayList<>(); + if (leftOptimizable) + { + newargs.addAll(optimizedChild.args); + for (int i = 1; i < node.args.size(); i++) + { + newargs.add(node.args.get(i)); + } + } + else + { + for (int i = 0; i < node.args.size() - 1; i++) + { + newargs.add(node.args.get(i)); + } + newargs.addAll(optimizedChild.args); + } + + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info(" - " + symbol + "(" + node.args.size() + ") => " + function + "(" + newargs.size() + ") at line " + (node.token.lineno + 1) + ", node depth " + indent); + } + node.token.morph(Tokenizer.Token.TokenType.FUNCTION, function); + node.args = newargs; + } + } + } + return optimized; + } + + private boolean optimizeTree(Context ctx, ExpressionNode node, Context.Type expectedType, int indent, boolean scriptsDebugging) + { + // ctx is just to report errors, not values evaluation + boolean optimized = false; + Tokenizer.Token.TokenType token = node.token.type; + if (!token.isFunctional()) + { + return false; + } + String symbol = node.token.surface; + + // input special cases here, like function signature + if (node.op instanceof LazyValue.Constant) + { + return false; // optimized already + } + // function or operator + + Fluff.EvalNode operation = ((token == Tokenizer.Token.TokenType.FUNCTION) ? functions : operators).get(symbol); + Context.Type requestedType = operation.staticType(expectedType); + for (ExpressionNode arg : node.args) + { + if (optimizeTree(ctx, arg, requestedType, indent + 1, scriptsDebugging)) + { + optimized = true; + } + } + + for (ExpressionNode arg : node.args) + { + if (arg.token.type.isConstant()) + { + continue; + } + if (arg.op instanceof LazyValue.ContextFreeLazyValue) + { + continue; + } + return optimized; + } + // a few exceptions which we don't implement in the framework for simplicity for now + if (!operation.pure()) + { + if (!symbol.equals("->") || expectedType != Context.Type.MAPDEF) + { + return optimized; + } + } + // element access with constant elements will always resolve the same way. + if (operation.pure() && symbol.equals(":") && expectedType == Context.Type.LVALUE) + { + expectedType = Context.Type.NONE; + } + List args = new ArrayList<>(node.args.size()); + for (ExpressionNode arg : node.args) + { + try + { + if (arg.op instanceof LazyValue.Constant) + { + Value val = ((LazyValue.Constant) arg.op).get(); + args.add((c, t) -> val); + } + else + { + args.add((c, t) -> arg.op.evalValue(ctx, requestedType)); + } + } + catch (NullPointerException npe) + { + throw new ExpressionException(ctx, this, node.token, "Attempted to evaluate context free expression"); + } + } + // applying argument unpacking + args = AbstractLazyFunction.lazify(AbstractLazyFunction.unpackLazy(args, ctx, requestedType)); + Value result; + if (operation instanceof ILazyFunction) + { + result = ((ILazyFunction) operation).lazyEval(ctx, expectedType, this, node.token, args).evalValue(null, expectedType); + } + else if (args.size() == 1) + { + result = ((ILazyOperator) operation).lazyEval(ctx, expectedType, this, node.token, args.get(0), null).evalValue(null, expectedType); + } + else // args == 2 + { + result = ((ILazyOperator) operation).lazyEval(ctx, expectedType, this, node.token, args.get(0), args.get(1)).evalValue(null, expectedType); + } + node.op = LazyValue.ofConstant(result); + if (scriptsDebugging) + { + CarpetScriptServer.LOG.info(" - " + symbol + "(" + args.stream().map(a -> a.evalValue(null, requestedType).getString()).collect(Collectors.joining(", ")) + ") => " + result.getString() + " at line " + (node.token.lineno + 1) + ", node depth " + indent); + } + return true; + } + + private LazyValue extractOp(Context ctx, ExpressionNode node, Context.Type expectedType) + { + if (node.op instanceof LazyValue.Constant) + { + // constants are immutable + if (node.token.type.isConstant()) + { + Value value = ((LazyValue.Constant) node.op).get(); + return (c, t) -> value; + } + return node.op; + } + if (node.op instanceof LazyValue.ContextFreeLazyValue) + { + Value ret = ((LazyValue.ContextFreeLazyValue) node.op).evalType(expectedType); + return (c, t) -> ret; + } + Tokenizer.Token token = node.token; + switch (token.type) + { + case UNARY_OPERATOR: + { + ILazyOperator op = operators.get(token.surface); + Context.Type requestedType = op.staticType(expectedType); + LazyValue arg = extractOp(ctx, node.args.get(0), requestedType); + return (c, t) -> op.lazyEval(c, t, this, token, arg, null).evalValue(c, t); + } + case OPERATOR: + { + ILazyOperator op = operators.get(token.surface); + Context.Type requestedType = op.staticType(expectedType); + LazyValue arg = extractOp(ctx, node.args.get(0), requestedType); + LazyValue arh = extractOp(ctx, node.args.get(1), requestedType); + return (c, t) -> op.lazyEval(c, t, this, token, arg, arh).evalValue(c, t); + } + case VARIABLE: + return (c, t) -> getOrSetAnyVariable(c, token.surface).evalValue(c, t); + case FUNCTION: + { + ILazyFunction f = functions.get(token.surface); + Context.Type requestedType = f.staticType(expectedType); + List params = node.args.stream().map(n -> extractOp(ctx, n, requestedType)).collect(Collectors.toList()); + return (c, t) -> f.lazyEval(c, t, this, token, params).evalValue(c, t); + } + case CONSTANT: + return node.op; + default: + throw new ExpressionException(ctx, this, node.token, "Unexpected token '" + node.token.type + " " + node.token.surface + "'"); + + } + } + + private void validate(Context c, List rpn) + { + /*- + * Thanks to Norman Ramsey: + * http://http://stackoverflow.com/questions/789847/postfix-notation-validation + */ + // each push on to this stack is a new function scope, with the value of + // each + // layer on the stack being the count of the number of parameters in + // that scope + IntArrayList stack = new IntArrayList(); // IntArrayList instead of just IntStack because we need to query the size + + // push the 'global' scope + stack.push(0); + + for (Tokenizer.Token token : rpn) + { + switch (token.type) + { + case UNARY_OPERATOR: + if (stack.topInt() < 1) + { + throw new ExpressionException(c, this, token, "Missing parameter(s) for operator " + token); + } + break; + case OPERATOR: + if (stack.topInt() < 2) + { + if (token.surface.equals(";")) + { + throw new ExpressionException(c, this, token, "Empty expression found for ';'"); + } + throw new ExpressionException(c, this, token, "Missing parameter(s) for operator " + token); + } + // pop the operator's 2 parameters and add the result + stack.set(stack.size() - 1, stack.topInt() - 2 + 1); + break; + case FUNCTION: + //ILazyFunction f = functions.get(token.surface);// don't validate global - userdef functions + //int numParams = stack.pop(); + //if (f != null && !f.numParamsVaries() && numParams != f.getNumParams()) + //{ + // throw new ExpressionException(c, this, token, "Function " + token + " expected " + f.getNumParams() + " parameters, got " + numParams); + //} + stack.popInt(); + // due to unpacking, all functions can have variable number of arguments + // we will be checking that at runtime. + // TODO try analyze arguments and assess if its possible that they are static + if (stack.size() <= 0) + { + throw new ExpressionException(c, this, token, "Too many function calls, maximum scope exceeded"); + } + // push the result of the function + stack.set(stack.size() - 1, stack.topInt() + 1); + break; + case OPEN_PAREN: + stack.push(0); + break; + default: + stack.set(stack.size() - 1, stack.topInt() + 1); + } + } + + if (stack.size() > 1) + { + throw new ExpressionException(c, this, "Too many unhandled function parameter lists"); + } + else if (stack.topInt() > 1) + { + throw new ExpressionException(c, this, "Too many numbers or variables"); + } + else if (stack.topInt() < 1) + { + throw new ExpressionException(c, this, "Empty expression"); + } + } +} diff --git a/src/main/java/carpet/script/Fluff.java b/src/main/java/carpet/script/Fluff.java new file mode 100644 index 0000000..0ab2e11 --- /dev/null +++ b/src/main/java/carpet/script/Fluff.java @@ -0,0 +1,337 @@ +package carpet.script; + +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.FunctionUnpackedArgumentsValue; +import carpet.script.value.ListValue; +import carpet.script.value.Value; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Fluff +{ + @FunctionalInterface + public interface TriFunction + { + R apply(A a, B b, C c); + } + + @FunctionalInterface + public interface QuadFunction + { + R apply(A a, B b, C c, D d); + } + + @FunctionalInterface + public interface QuinnFunction + { + R apply(A a, B b, C c, D d, E e); + } + + @FunctionalInterface + public interface SexFunction + { + R apply(A a, B b, C c, D d, E e, F f); + } + + public interface UsageProvider + { + String getUsage(); + } + + public interface EvalNode + { + /** + * @return true if function has constant output if arguments are constant and can be evaluated + * statically (without context) + */ + boolean pure(); + + /** + * @return true if function has constant output if arguments are constant and can be evaluated + * statically (without context) + */ + boolean transitive(); + + /** + * @return required argument eval type in case its evaluated statically without context but with a given context type + */ + default Context.Type staticType(Context.Type outerType) + { + return transitive() ? outerType : Context.NONE; + } + + } + + public interface ILazyFunction extends EvalNode + { + int getNumParams(); + + boolean numParamsVaries(); + + LazyValue lazyEval(Context c, Context.Type type, Expression expr, Tokenizer.Token token, List lazyParams); + + static void checkInterrupts() + { + if (ScriptHost.mainThread != Thread.currentThread() && Thread.currentThread().isInterrupted()) + { + throw new InternalExpressionException("Thread interrupted"); + } + } + // lazy function has a chance to change execution based on context + } + + public interface IFunction extends ILazyFunction + { + Value eval(List parameters); + } + + public interface ILazyOperator extends EvalNode + { + int getPrecedence(); + + boolean isLeftAssoc(); + + LazyValue lazyEval(Context c, Context.Type type, Expression e, Tokenizer.Token t, LazyValue v1, LazyValue v2); + } + + public interface IOperator extends ILazyOperator + { + Value eval(Value v1, Value v2); + } + + public abstract static class AbstractLazyFunction implements ILazyFunction + { + protected String name; + int numParams; + + public AbstractLazyFunction(int numParams, String name) + { + super(); + this.numParams = numParams; + this.name = name; + } + + + public String getName() + { + return name; + } + + @Override + public int getNumParams() + { + return numParams; + } + + @Override + public boolean numParamsVaries() + { + return numParams < 0; + } + + public static List unpackLazy(List lzargs, Context c, Context.Type contextType) + { + List args = new ArrayList<>(); + for (LazyValue lv : lzargs) + { + Value arg = lv.evalValue(c, contextType); + if (arg instanceof FunctionUnpackedArgumentsValue) + { + args.addAll(((ListValue) arg).getItems()); + } + else + { + args.add(arg); + } + } + return args; + } + + public List unpackArgs(List lzargs, Context c, Context.Type contextType) + { + List args = unpackLazy(lzargs, c, contextType); + if (!numParamsVaries() && getNumParams() != args.size()) + { + throw new InternalExpressionException("Function " + getName() + " expected " + getNumParams() + " parameters, got " + args.size()); + } + return args; + } + + public static List lazify(List args) + { + List lzargs = new ArrayList<>(args.size()); + args.forEach(v -> lzargs.add((c, t) -> v)); + return lzargs; + } + } + + public abstract static class AbstractFunction extends AbstractLazyFunction implements IFunction + { + AbstractFunction(int numParams, String name) + { + super(numParams, name); + } + + @Override + public boolean pure() + { + return true; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context cc, Context.Type type, Expression e, Tokenizer.Token t, List lazyParams) + { + + return new LazyValue() + { // eager evaluation always ignores the required type and evals params by none default + private List params; + + @Override + public Value evalValue(Context c, Context.Type type) + { + ILazyFunction.checkInterrupts(); + try + { + return AbstractFunction.this.eval(getParams(c)); + } + catch (RuntimeException exc) + { + throw Expression.handleCodeException(cc, exc, e, t); + } + } + + private List getParams(Context c) + { + if (params == null) + { + // very likely needs to be dynamic, so not static like here, or remember if it was. + params = unpackArgs(lazyParams, c, Context.Type.NONE); + } + else + { + CarpetScriptServer.LOG.error("How did we get here 1"); + } + return params; + } + }; + } + } + + public abstract static class AbstractLazyOperator implements ILazyOperator + { + int precedence; + + boolean leftAssoc; + + AbstractLazyOperator(int precedence, boolean leftAssoc) + { + super(); + this.precedence = precedence; + this.leftAssoc = leftAssoc; + } + + @Override + public int getPrecedence() + { + return precedence; + } + + @Override + public boolean isLeftAssoc() + { + return leftAssoc; + } + + } + + public abstract static class AbstractOperator extends AbstractLazyOperator implements IOperator + { + + AbstractOperator(int precedence, boolean leftAssoc) + { + super(precedence, leftAssoc); + } + + @Override + public boolean pure() + { + return true; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context cc, Context.Type type, Expression e, Tokenizer.Token t, LazyValue v1, LazyValue v2) + { + return (c, typeIgnored) -> { + try + { + return AbstractOperator.this.eval(v1.evalValue(c, Context.Type.NONE), v2.evalValue(c, Context.Type.NONE)); + } + catch (RuntimeException exc) + { + throw Expression.handleCodeException(cc, exc, e, t); + } + }; + } + } + + public abstract static class AbstractUnaryOperator extends AbstractOperator + { + AbstractUnaryOperator(int precedence, boolean leftAssoc) + { + super(precedence, leftAssoc); + } + + @Override + public boolean pure() + { + return true; + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public LazyValue lazyEval(Context cc, Context.Type type, Expression e, Tokenizer.Token t, LazyValue v1, LazyValue v2) + { + if (v2 != null) + { + throw new ExpressionException(cc, e, t, "Did not expect a second parameter for unary operator"); + } + return (c, ignoredType) -> { + try + { + return AbstractUnaryOperator.this.evalUnary(v1.evalValue(c, Context.Type.NONE)); + } + catch (RuntimeException exc) + { + throw Expression.handleCodeException(cc, exc, e, t); + } + }; + } + + @Override + public Value eval(Value v1, Value v2) + { + throw new IllegalStateException("Shouldn't end up here"); + } + + public abstract Value evalUnary(Value v1); + } +} diff --git a/src/main/java/carpet/script/LazyValue.java b/src/main/java/carpet/script/LazyValue.java new file mode 100644 index 0000000..ab616a6 --- /dev/null +++ b/src/main/java/carpet/script/LazyValue.java @@ -0,0 +1,69 @@ +package carpet.script; + +import carpet.script.value.Value; + +/** + * LazyNumber interface created for lazily evaluated functions + */ +@FunctionalInterface +public interface LazyValue +{ + LazyValue FALSE = (c, t) -> Value.FALSE; + LazyValue TRUE = (c, t) -> Value.TRUE; + LazyValue NULL = (c, t) -> Value.NULL; + LazyValue ZERO = (c, t) -> Value.ZERO; + + static LazyValue ofConstant(Value val) + { + return new Constant(val); + } + + Value evalValue(Context c, Context.Type type); + + default Value evalValue(Context c) + { + return evalValue(c, Context.Type.NONE); + } + + @FunctionalInterface + interface ContextFreeLazyValue extends LazyValue + { + + Value evalType(Context.Type type); + + @Override + default Value evalValue(Context c, Context.Type type) + { + return evalType(type); + } + } + + + class Constant implements ContextFreeLazyValue + { + Value result; + + public Constant(Value value) + { + result = value; + } + + public Value get() + { + return result; + } + + @Override + public Value evalType(Context.Type type) + { + + return result.fromConstant(); + } + + @Override + public Value evalValue(Context c, Context.Type type) + { + return result.fromConstant(); + } + } +} diff --git a/src/main/java/carpet/script/Module.java b/src/main/java/carpet/script/Module.java new file mode 100644 index 0000000..30264e6 --- /dev/null +++ b/src/main/java/carpet/script/Module.java @@ -0,0 +1,131 @@ +package carpet.script; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Objects; + +import org.apache.commons.io.IOUtils; + +import carpet.script.argument.FileArgument; +import net.minecraft.nbt.Tag; + +public record Module(String name, String code, boolean library) +{ + public Module + { + Objects.requireNonNull(name); + Objects.requireNonNull(code); + } + + public static Module fromPath(Path path) + { + boolean library = path.getFileName().toString().endsWith(".scl"); + try + { + String name = path.getFileName().toString().replaceFirst("\\.scl?", "").toLowerCase(Locale.ROOT); + String code = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + return new Module(name, code, library); + } + catch (IOException e) + { + throw new IllegalArgumentException("Failed to load scarpet module", e); + } + } + + /** + * Creates a new {@link Module} with an app located in Carpet's JAR. + * + * @param scriptName A {@link String} being the name of the script. The extension will be autocompleted + * @param isLibrary A {@link boolean} indicating whether or not the script is a library + * @return The created {@link BundledModule} + */ + public static Module carpetNative(String scriptName, boolean isLibrary) + { + return fromJarPath("assets/carpet/scripts/", scriptName, isLibrary); + } + + /** + * Creates a new {@link Module} with an app located at a specified path inside some mod's JAR. + * + * @param path A {@link String} being the path to the directory where the app is located. + * @param scriptName A {@link String} being the name of the script. The extension will be autocompleted + * @param isLibrary A {@link boolean} indicating whether or not the script is a library + * @return The created {@link BundledModule} + * @see #fromJarPathWithCustomName(String, String, boolean) + */ + public static Module fromJarPath(String path, String scriptName, boolean isLibrary) + { + return fromJarPathWithCustomName(path + scriptName + (isLibrary ? ".scl" : ".sc"), scriptName, isLibrary); + } + + /** + * Creates a new {@link Module} with an app located at the specified fullPath (inside a mod jar)with a custom name. + * + * @param fullPath A {@link String} being the full path to the app's code, including file and extension. + * @param customName A {@link String} being the custom name for the script. + * @param isLibrary A {@link boolean} indicating whether or not the script is a library + * @return The created {@link Module} + * @see #fromJarPath(String, String, boolean) + */ + public static Module fromJarPathWithCustomName(String fullPath, String customName, boolean isLibrary) + { + try + { + String name = customName.toLowerCase(Locale.ROOT); + String code = IOUtils.toString( + Module.class.getClassLoader().getResourceAsStream(fullPath), + StandardCharsets.UTF_8 + ); + return new Module(name, code, isLibrary); + } + catch (IOException e) + { + throw new IllegalArgumentException("Failed to load bundled module", e); + } + } + + public static Tag getData(Module module, ScriptServer scriptServer) + { + Path dataFile = resolveResource(module, scriptServer); + if (dataFile == null || !Files.exists(dataFile) || !(Files.isRegularFile(dataFile))) + { + return null; + } + synchronized (FileArgument.writeIOSync) + { + return FileArgument.readTag(dataFile); + } + } + + public static void saveData(Module module, Tag globalState, ScriptServer scriptServer) + { + Path dataFile = resolveResource(module, scriptServer); + if (dataFile == null) + { + return; + } + if (!Files.exists(dataFile.getParent())) + { + try + { + Files.createDirectories(dataFile.getParent()); + } + catch (IOException e) + { + throw new IllegalStateException(e); + } + } + synchronized (FileArgument.writeIOSync) + { + FileArgument.writeTagDisk(globalState, dataFile, false); + } + } + + private static Path resolveResource(Module module, ScriptServer scriptServer) + { + return module == null ? null : scriptServer.resolveResource(module.name() + ".data.nbt"); + } +} diff --git a/src/main/java/carpet/script/ScriptCommand.java b/src/main/java/carpet/script/ScriptCommand.java new file mode 100644 index 0000000..e8bc716 --- /dev/null +++ b/src/main/java/carpet/script/ScriptCommand.java @@ -0,0 +1,714 @@ +package carpet.script; + +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.utils.AppStoreManager; +import carpet.script.exception.CarpetExpressionException; +import carpet.script.value.FunctionValue; +import carpet.script.value.Value; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandBuildContext; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.blocks.BlockInput; +import net.minecraft.commands.arguments.blocks.BlockPredicateArgument; +import net.minecraft.commands.arguments.blocks.BlockStateArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Clearable; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.level.levelgen.structure.BoundingBox; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; +import static net.minecraft.commands.SharedSuggestionProvider.suggest; + +public class ScriptCommand +{ + private static final TreeSet scarpetFunctions; + private static final TreeSet APIFunctions; + + static + { + Set allFunctions = (new CarpetExpression(null, "null", null, null)).getExpr().getFunctionNames(); + scarpetFunctions = new TreeSet<>(Expression.none.getFunctionNames()); + APIFunctions = allFunctions.stream().filter(s -> !scarpetFunctions.contains(s)).collect(Collectors.toCollection(TreeSet::new)); + } + + public static List suggestFunctions(ScriptHost host, String previous, String prefix) + { + previous = previous.replace("\\'", ""); + int quoteCount = StringUtils.countMatches(previous, '\''); + if (quoteCount % 2 == 1) + { + return Collections.emptyList(); + } + int maxLen = prefix.length() < 3 ? (prefix.length() * 2 + 1) : 1234; + String eventPrefix = prefix.startsWith("__on_") ? prefix.substring(5) : null; + List scarpetMatches = scarpetFunctions.stream(). + filter(s -> s.startsWith(prefix) && s.length() <= maxLen).map(s -> s + "(").collect(Collectors.toList()); + scarpetMatches.addAll(APIFunctions.stream(). + filter(s -> s.startsWith(prefix) && s.length() <= maxLen).map(s -> s + "(").toList()); + // not that useful in commandline, more so in external scripts, so skipping here + if (eventPrefix != null) + { + scarpetMatches.addAll(CarpetEventServer.Event.publicEvents(null).stream(). + filter(e -> e.name.startsWith(eventPrefix)).map(s -> "__on_" + s.name + "(").toList()); + } + scarpetMatches.addAll(host.globalFunctionNames(host.main, s -> s.startsWith(prefix)).map(s -> s + "(").toList()); + scarpetMatches.addAll(host.globalVariableNames(host.main, s -> s.startsWith(prefix)).toList()); + return scarpetMatches; + } + + private static CompletableFuture suggestCode( + CommandContext context, + SuggestionsBuilder suggestionsBuilder + ) throws CommandSyntaxException + { + CarpetScriptHost currentHost = getHost(context); + String previous = suggestionsBuilder.getRemaining(); + int strlen = previous.length(); + StringBuilder lastToken = new StringBuilder(); + for (int idx = strlen - 1; idx >= 0; idx--) + { + char ch = previous.charAt(idx); + if (Character.isLetterOrDigit(ch) || ch == '_') + { + lastToken.append(ch); + } + else + { + break; + } + } + if (lastToken.length() == 0) + { + return suggestionsBuilder.buildFuture(); + } + String prefix = lastToken.reverse().toString(); + String previousString = previous.substring(0, previous.length() - prefix.length()); + suggestFunctions(currentHost, previousString, prefix).forEach(text -> suggestionsBuilder.suggest(previousString + text)); + return suggestionsBuilder.buildFuture(); + } + + /** + * A method to suggest the available scarpet scripts based off of the current player input and {@link AppStoreManager#APP_STORE_ROOT} + * variable. + */ + private static CompletableFuture suggestDownloadableApps( + CommandContext context, + SuggestionsBuilder suggestionsBuilder + ) throws CommandSyntaxException + { + + return CompletableFuture.supplyAsync(() -> { + String previous = suggestionsBuilder.getRemaining(); + try + { + AppStoreManager.suggestionsFromPath(previous, context.getSource()).forEach(suggestionsBuilder::suggest); + } + catch (IOException e) + { + CarpetScriptServer.LOG.warn("Exception when fetching app store structure", e); + } + return suggestionsBuilder.build(); + }); + } + + private static CarpetScriptServer ss(CommandContext context) + { + return Vanilla.MinecraftServer_getScriptServer(context.getSource().getServer()); + } + + public static void register(CommandDispatcher dispatcher, CommandBuildContext commandBuildContext) + { + LiteralArgumentBuilder b = literal("globals"). + executes(context -> listGlobals(context, false)). + then(literal("all").executes(context -> listGlobals(context, true))); + LiteralArgumentBuilder o = literal("stop"). + executes((cc) -> { + ss(cc).stopAll = true; + return 1; + }); + LiteralArgumentBuilder u = literal("resume"). + executes((cc) -> { + ss(cc).stopAll = false; + return 1; + }); + LiteralArgumentBuilder l = literal("run"). + requires(Vanilla::ServerPlayer_canScriptACE). + then(argument("expr", StringArgumentType.greedyString()).suggests(ScriptCommand::suggestCode). + executes((cc) -> compute( + cc, + StringArgumentType.getString(cc, "expr")))); + LiteralArgumentBuilder s = literal("invoke"). + then(argument("call", StringArgumentType.word()).suggests((cc, bb) -> suggest(suggestFunctionCalls(cc), bb)). + executes((cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + null, + null, + "" + )). + then(argument("arguments", StringArgumentType.greedyString()). + executes((cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + null, + null, + StringArgumentType.getString(cc, "arguments") + )))); + LiteralArgumentBuilder c = literal("invokepoint"). + then(argument("call", StringArgumentType.word()).suggests((cc, bb) -> suggest(suggestFunctionCalls(cc), bb)). + then(argument("origin", BlockPosArgument.blockPos()). + executes((cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgument.getSpawnablePos(cc, "origin"), + null, + "" + )). + then(argument("arguments", StringArgumentType.greedyString()). + executes((cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgument.getSpawnablePos(cc, "origin"), + null, + StringArgumentType.getString(cc, "arguments") + ))))); + LiteralArgumentBuilder h = literal("invokearea"). + then(argument("call", StringArgumentType.word()).suggests((cc, bb) -> suggest(suggestFunctionCalls(cc), bb)). + then(argument("from", BlockPosArgument.blockPos()). + then(argument("to", BlockPosArgument.blockPos()). + executes((cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + "" + )). + then(argument("arguments", StringArgumentType.greedyString()). + executes((cc) -> invoke( + cc, + StringArgumentType.getString(cc, "call"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + StringArgumentType.getString(cc, "arguments") + )))))); + LiteralArgumentBuilder i = literal("scan").requires((player) -> player.hasPermission(2)). + then(argument("origin", BlockPosArgument.blockPos()). + then(argument("from", BlockPosArgument.blockPos()). + then(argument("to", BlockPosArgument.blockPos()). + then(argument("expr", StringArgumentType.greedyString()). + suggests(ScriptCommand::suggestCode). + executes((cc) -> scriptScan( + cc, + BlockPosArgument.getSpawnablePos(cc, "origin"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + StringArgumentType.getString(cc, "expr") + )))))); + LiteralArgumentBuilder e = literal("fill").requires((player) -> player.hasPermission(2)). + then(argument("origin", BlockPosArgument.blockPos()). + then(argument("from", BlockPosArgument.blockPos()). + then(argument("to", BlockPosArgument.blockPos()). + then(argument("expr", StringArgumentType.string()). + then(argument("block", BlockStateArgument.block(commandBuildContext)). + executes((cc) -> scriptFill( + cc, + BlockPosArgument.getSpawnablePos(cc, "origin"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgument.getBlock(cc, "block"), + null, "solid" + )). + then(literal("replace"). + then(argument("filter", BlockPredicateArgument.blockPredicate(commandBuildContext)) + .executes((cc) -> scriptFill( + cc, + BlockPosArgument.getSpawnablePos(cc, "origin"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgument.getBlock(cc, "block"), + BlockPredicateArgument.getBlockPredicate(cc, "filter"), + "solid" + ))))))))); + LiteralArgumentBuilder t = literal("outline").requires((player) -> player.hasPermission(2)). + then(argument("origin", BlockPosArgument.blockPos()). + then(argument("from", BlockPosArgument.blockPos()). + then(argument("to", BlockPosArgument.blockPos()). + then(argument("expr", StringArgumentType.string()). + then(argument("block", BlockStateArgument.block(commandBuildContext)). + executes((cc) -> scriptFill( + cc, + BlockPosArgument.getSpawnablePos(cc, "origin"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgument.getBlock(cc, "block"), + null, "outline" + )). + then(literal("replace"). + then(argument("filter", BlockPredicateArgument.blockPredicate(commandBuildContext)) + .executes((cc) -> scriptFill( + cc, + BlockPosArgument.getSpawnablePos(cc, "origin"), + BlockPosArgument.getSpawnablePos(cc, "from"), + BlockPosArgument.getSpawnablePos(cc, "to"), + StringArgumentType.getString(cc, "expr"), + BlockStateArgument.getBlock(cc, "block"), + BlockPredicateArgument.getBlockPredicate(cc, "filter"), + "outline" + ))))))))); + LiteralArgumentBuilder a = literal("load").requires(Vanilla::ServerPlayer_canScriptACE). + then(argument("app", StringArgumentType.word()). + suggests((cc, bb) -> suggest(ss(cc).listAvailableModules(true), bb)). + executes((cc) -> + { + boolean success = ss(cc).addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), null, true, false, false, null); + return success ? 1 : 0; + }). + then(literal("global"). + executes((cc) -> + { + boolean success = ss(cc).addScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), null, false, false, false, null); + return success ? 1 : 0; + } + ) + ) + ); + LiteralArgumentBuilder f = literal("unload").requires(Vanilla::ServerPlayer_canScriptACE). + then(argument("app", StringArgumentType.word()). + suggests((cc, bb) -> suggest(ss(cc).unloadableModules, bb)). + executes((cc) -> + { + boolean success = ss(cc).removeScriptHost(cc.getSource(), StringArgumentType.getString(cc, "app"), true, false); + return success ? 1 : 0; + })); + + LiteralArgumentBuilder q = literal("event").requires(Vanilla::ServerPlayer_canScriptACE). + executes(ScriptCommand::listEvents). + then(literal("add_to"). + then(argument("event", StringArgumentType.word()). + suggests((cc, bb) -> suggest(CarpetEventServer.Event.publicEvents(ss(cc)).stream().map(ev -> ev.name).collect(Collectors.toList()), bb)). + then(argument("call", StringArgumentType.word()). + suggests((cc, bb) -> suggest(suggestFunctionCalls(cc), bb)). + executes((cc) -> ss(cc).events.addEventFromCommand( + cc.getSource(), + StringArgumentType.getString(cc, "event"), + null, + StringArgumentType.getString(cc, "call") + ) ? 1 : 0)). + then(literal("from"). + then(argument("app", StringArgumentType.word()). + suggests((cc, bb) -> suggest(ss(cc).modules.keySet(), bb)). + then(argument("call", StringArgumentType.word()). + suggests((cc, bb) -> suggest(suggestFunctionCalls(cc), bb)). + executes((cc) -> ss(cc).events.addEventFromCommand( + cc.getSource(), + StringArgumentType.getString(cc, "event"), + StringArgumentType.getString(cc, "app"), + StringArgumentType.getString(cc, "call") + ) ? 1 : 0)))))). + then(literal("remove_from"). + then(argument("event", StringArgumentType.word()). + suggests((cc, bb) -> suggest(CarpetEventServer.Event.publicEvents(ss(cc)).stream().filter(CarpetEventServer.Event::isNeeded).map(ev -> ev.name).collect(Collectors.toList()), bb)). + then(argument("call", StringArgumentType.greedyString()). + suggests((cc, bb) -> suggest(CarpetEventServer.Event.getEvent(StringArgumentType.getString(cc, "event"), ss(cc)).handler.inspectCurrentCalls().stream().map(CarpetEventServer.Callback::toString), bb)). + executes((cc) -> ss(cc).events.removeEventFromCommand( + cc.getSource(), + StringArgumentType.getString(cc, "event"), + StringArgumentType.getString(cc, "call") + ) ? 1 : 0)))); + + LiteralArgumentBuilder d = literal("download").requires(Vanilla::ServerPlayer_canScriptACE). + then(argument("path", StringArgumentType.greedyString()). + suggests(ScriptCommand::suggestDownloadableApps). + executes(cc -> AppStoreManager.downloadScript(cc.getSource(), StringArgumentType.getString(cc, "path")))); + LiteralArgumentBuilder r = literal("remove").requires(Vanilla::ServerPlayer_canScriptACE). + then(argument("app", StringArgumentType.word()). + suggests((cc, bb) -> suggest(ss(cc).unloadableModules, bb)). + executes((cc) -> + { + boolean success = ss(cc).uninstallApp(cc.getSource(), StringArgumentType.getString(cc, "app")); + return success ? 1 : 0; + })); + + dispatcher.register(literal("script"). + requires(Vanilla::ServerPlayer_canScriptGeneral). + then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t).then(a).then(f).then(q).then(d).then(r)); + dispatcher.register(literal("script"). + requires(Vanilla::ServerPlayer_canScriptGeneral). + then(literal("in"). + then(argument("app", StringArgumentType.word()). + suggests((cc, bb) -> suggest(ss(cc).modules.keySet(), bb)). + then(b).then(u).then(o).then(l).then(s).then(c).then(h).then(i).then(e).then(t)))); + } + + private static CarpetScriptHost getHost(CommandContext context) throws CommandSyntaxException + { + CarpetScriptHost host; + CarpetScriptServer scriptServer = ss(context); + try + { + String name = StringArgumentType.getString(context, "app").toLowerCase(Locale.ROOT); + CarpetScriptHost parentHost = scriptServer.modules.getOrDefault(name, scriptServer.globalHost); + host = parentHost.retrieveOwnForExecution(context.getSource()); + } + catch (IllegalArgumentException ignored) + { + host = scriptServer.globalHost; + } + host.setChatErrorSnooper(context.getSource()); + return host; + } + + private static Collection suggestFunctionCalls(CommandContext c) throws CommandSyntaxException + { + CarpetScriptHost host = getHost(c); + return host.globalFunctionNames(host.main, s -> !s.startsWith("_")).sorted().collect(Collectors.toList()); + } + + private static int listEvents(CommandContext context) + { + CarpetScriptServer scriptServer = ss(context); + CommandSourceStack source = context.getSource(); + Carpet.Messenger_message(source, "w Lists ALL event handlers:"); + for (CarpetEventServer.Event event : CarpetEventServer.Event.getAllEvents(scriptServer, null)) + { + boolean shownEvent = false; + for (CarpetEventServer.Callback c : event.handler.inspectCurrentCalls()) + { + if (!shownEvent) + { + Carpet.Messenger_message(source, "w Handlers for " + event.name + ": "); + shownEvent = true; + } + Carpet.Messenger_message(source, "w - " + c.function.getString() + (c.host == null ? "" : " (from " + c.host + ")")); + } + } + return 1; + } + + private static int listGlobals(CommandContext context, boolean all) throws CommandSyntaxException + { + CarpetScriptHost host = getHost(context); + CommandSourceStack source = context.getSource(); + CarpetScriptServer scriptServer = ss(context); + + Carpet.Messenger_message(source, "lb Stored functions" + ((host == scriptServer.globalHost) ? ":" : " in " + host.getVisualName() + ":")); + host.globalFunctionNames(host.main, (str) -> all || !str.startsWith("__")).sorted().forEach((s) -> { + FunctionValue fun = host.getFunction(s); + if (fun == null) + { + Carpet.Messenger_message(source, "gb " + s, "g - unused import"); + Carpet.Messenger_message(source, "gi ----------------"); + return; + } + Expression expr = fun.getExpression(); + Tokenizer.Token tok = fun.getToken(); + List snippet = expr.getExpressionSnippet(tok); + Carpet.Messenger_message(source, "wb " + fun.fullName(), "t defined at: line " + (tok.lineno + 1) + " pos " + (tok.linepos + 1)); + for (String snippetLine : snippet) + { + Carpet.Messenger_message(source, "w " + snippetLine); + } + Carpet.Messenger_message(source, "gi ----------------"); + }); + //Messenger.m(source, "w "+code); + Carpet.Messenger_message(source, "w "); + Carpet.Messenger_message(source, "lb Global variables" + ((host == scriptServer.globalHost) ? ":" : " in " + host.getVisualName() + ":")); + host.globalVariableNames(host.main, (s) -> s.startsWith("global_")).sorted().forEach((s) -> { + LazyValue variable = host.getGlobalVariable(s); + if (variable == null) + { + Carpet.Messenger_message(source, "gb " + s, "g - unused import"); + } + else + { + Carpet.Messenger_message(source, "wb " + s + ": ", "w " + variable.evalValue(null).getPrettyString()); + } + }); + return 1; + } + + public static int handleCall(CommandSourceStack source, CarpetScriptHost host, Supplier call) + { + try + { + Runnable token = Carpet.startProfilerSection("Scarpet run"); + host.setChatErrorSnooper(source); + long start = System.nanoTime(); + Value result = call.get(); + long time = ((System.nanoTime() - start) / 1000); + String metric = "\u00B5s"; + if (time > 5000) + { + time /= 1000; + metric = "ms"; + } + if (time > 10000) + { + time /= 1000; + metric = "s"; + } + Carpet.Messenger_message(source, "wi = ", "wb " + result.getString(), "gi (" + time + metric + ")"); + int intres = (int) result.readInteger(); + token.run(); + return intres; + } + catch (CarpetExpressionException e) + { + host.handleErrorWithStack("Error while evaluating expression", e); + } + catch (ArithmeticException ae) + { + host.handleErrorWithStack("Math doesn't compute", ae); + } + catch (StackOverflowError soe) + { + host.handleErrorWithStack("Your thoughts are too deep", soe); + } + return 0; + //host.resetErrorSnooper(); // lets say no need to reset the snooper in case something happens on the way + } + + private static int invoke(CommandContext context, String call, BlockPos pos1, BlockPos pos2, String args) throws CommandSyntaxException + { + CommandSourceStack source = context.getSource(); + CarpetScriptHost host = getHost(context); + if (call.startsWith("__")) + { + Carpet.Messenger_message(source, "r Hidden functions are only callable in scripts"); + return 0; + } + List positions = new ArrayList<>(); + if (pos1 != null) + { + positions.add(pos1.getX()); + positions.add(pos1.getY()); + positions.add(pos1.getZ()); + } + if (pos2 != null) + { + positions.add(pos2.getX()); + positions.add(pos2.getY()); + positions.add(pos2.getZ()); + } + //if (!(args.trim().isEmpty())) + // arguments.addAll(Arrays.asList(args.trim().split("\\s+"))); + return handleCall(source, host, () -> host.callLegacy(source, call, positions, args)); + } + + + private static int compute(CommandContext context, String expr) throws CommandSyntaxException + { + CommandSourceStack source = context.getSource(); + CarpetScriptHost host = getHost(context); + return handleCall(source, host, () -> { + CarpetExpression ex = new CarpetExpression(host.main, expr, source, new BlockPos(0, 0, 0)); + return ex.scriptRunCommand(host, BlockPos.containing(source.getPosition())); + }); + } + + private static int scriptScan(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr) throws CommandSyntaxException + { + CommandSourceStack source = context.getSource(); + CarpetScriptHost host = getHost(context); + BoundingBox area = BoundingBox.fromCorners(a, b); + CarpetExpression cexpr = new CarpetExpression(host.main, expr, source, origin); + int int_1 = area.getXSpan() * area.getYSpan() * area.getZSpan(); // X Y Z + if (int_1 > Vanilla.MinecraftServer_getFillLimit(source.getServer()) ) + { + Carpet.Messenger_message(source, "r too many blocks to evaluate: " + int_1); + return 1; + } + int successCount = 0; + Carpet.getImpendingFillSkipUpdates().set(!Carpet.getFillUpdates()); + try + { + for (int x = area.minX(); x <= area.maxX(); x++) + { + for (int y = area.minY(); y <= area.maxY(); y++) + { + for (int z = area.minZ(); z <= area.maxZ(); z++) + { + try + { + if (cexpr.fillAndScanCommand(host, x, y, z)) + { + successCount++; + } + } + catch (ArithmeticException ignored) + { + } + } + } + } + } + catch (CarpetExpressionException exc) + { + host.handleErrorWithStack("Error while processing command", exc); + return 0; + } + finally + { + Carpet.getImpendingFillSkipUpdates().set(false); + } + Carpet.Messenger_message(source, "w Expression successful in " + successCount + " out of " + int_1 + " blocks"); + return successCount; + + } + + + private static int scriptFill(CommandContext context, BlockPos origin, BlockPos a, BlockPos b, String expr, + BlockInput block, Predicate replacement, String mode) throws CommandSyntaxException + { + CommandSourceStack source = context.getSource(); + CarpetScriptHost host = getHost(context); + BoundingBox area = BoundingBox.fromCorners(a, b); + CarpetExpression cexpr = new CarpetExpression(host.main, expr, source, origin); + int int_1 = area.getXSpan() * area.getYSpan() * area.getZSpan(); + if (int_1 > Vanilla.MinecraftServer_getFillLimit(source.getServer())) + { + Carpet.Messenger_message(source, "r too many blocks to evaluate: " + int_1); + return 1; + } + + boolean[][][] volume = new boolean[area.getXSpan()][area.getYSpan()][area.getZSpan()]; //X then Y then Z got messedup + + BlockPos.MutableBlockPos mbpos = origin.mutable(); + ServerLevel world = source.getLevel(); + + for (int x = area.minX(); x <= area.maxX(); x++) + { + for (int y = area.minY(); y <= area.maxY(); y++) + { + for (int z = area.minZ(); z <= area.maxZ(); z++) + { + try + { + if (cexpr.fillAndScanCommand(host, x, y, z)) + { + volume[x - area.minX()][y - area.minY()][z - area.minZ()] = true; + } + } + catch (CarpetExpressionException e) + { + host.handleErrorWithStack("Exception while filling the area", e); + return 0; + } + catch (ArithmeticException e) + { + } + } + } + } + int maxx = area.getXSpan() - 1; + int maxy = area.getYSpan() - 1; + int maxz = area.getZSpan() - 1; + if ("outline".equalsIgnoreCase(mode)) + { + boolean[][][] newVolume = new boolean[area.getXSpan()][area.getYSpan()][area.getZSpan()]; + for (int x = 0; x <= maxx; x++) + { + for (int y = 0; y <= maxy; y++) + { + for (int z = 0; z <= maxz; z++) + { + if (volume[x][y][z]) + { + if (((x != 0 && !volume[x - 1][y][z]) || + (x != maxx && !volume[x + 1][y][z]) || + (y != 0 && !volume[x][y - 1][z]) || + (y != maxy && !volume[x][y + 1][z]) || + (z != 0 && !volume[x][y][z - 1]) || + (z != maxz && !volume[x][y][z + 1]) + )) + { + newVolume[x][y][z] = true; + } + } + } + } + } + volume = newVolume; + } + int affected = 0; + + Carpet.getImpendingFillSkipUpdates().set(!Carpet.getFillUpdates()); + for (int x = 0; x <= maxx; x++) + { + for (int y = 0; y <= maxy; y++) + { + for (int z = 0; z <= maxz; z++) + { + if (volume[x][y][z]) + { + mbpos.set(x + area.minX(), y + area.minY(), z + area.minZ()); + if (replacement == null || replacement.test( + new BlockInWorld(world, mbpos, true))) + { + BlockEntity tileentity = world.getBlockEntity(mbpos); + Clearable.tryClear(tileentity); + + if (block.place(world, mbpos, 2)) + { + ++affected; + } + } + } + } + } + } + Carpet.getImpendingFillSkipUpdates().set(false); + + if (Carpet.getFillUpdates() && block != null) + { + for (int x = 0; x <= maxx; x++) + { + for (int y = 0; y <= maxy; y++) + { + for (int z = 0; z <= maxz; z++) + { + if (volume[x][y][z]) + { + mbpos.set(x + area.minX(), y + area.minY(), z + area.minZ()); + Block blokc = world.getBlockState(mbpos).getBlock(); + world.blockUpdated(mbpos, blokc); + } + } + } + } + } + Carpet.Messenger_message(source, "gi Affected " + affected + " blocks in " + area.getXSpan() * area.getYSpan() * area.getZSpan() + " block volume"); + return 1; + } +} + diff --git a/src/main/java/carpet/script/ScriptHost.java b/src/main/java/carpet/script/ScriptHost.java new file mode 100644 index 0000000..29cd523 --- /dev/null +++ b/src/main/java/carpet/script/ScriptHost.java @@ -0,0 +1,574 @@ +package carpet.script; + +import carpet.script.exception.ExpressionException; +import carpet.script.exception.IntegrityException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.FunctionValue; +import carpet.script.value.Value; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public abstract class ScriptHost +{ + private static final Map randomizers = new Long2ObjectOpenHashMap<>(); + + public static Thread mainThread = null; + private final Map executorServices = new HashMap<>(); + private final Map locks = new ConcurrentHashMap<>(); + private final ScriptServer scriptServer; + protected boolean inTermination = false; + public boolean strict; + + private final Set deprecations = new HashSet<>(); + + public Random getRandom(long aLong) + { + if (randomizers.size() > 65536) + { + randomizers.clear(); + } + return randomizers.computeIfAbsent(aLong, Random::new); + } + + public boolean resetRandom(long aLong) + { + return randomizers.remove(aLong) != null; + } + + public Path resolveScriptFile(String suffix) + { + return scriptServer.resolveResource(suffix); + } + + public boolean canSynchronouslyExecute() + { + return true; + } + + public static class ModuleData + { + Module parent; + public final Map globalFunctions = new Object2ObjectOpenHashMap<>(); + public final Map globalVariables = new Object2ObjectOpenHashMap<>(); + public final Map functionImports = new Object2ObjectOpenHashMap<>(); // imported functions string to module + public final Map globalsImports = new Object2ObjectOpenHashMap<>(); // imported global variables string to module + public final Map futureImports = new Object2ObjectOpenHashMap<>(); // imports not known before used + + public ModuleData(Module parent, ModuleData other) + { + super(); + // imports are just pointers, but they still point to the wrong modules (point to the parent) + this.parent = parent; + globalFunctions.putAll(other.globalFunctions); + other.globalVariables.forEach((key, value) -> + { + Value var = value.evalValue(null); + Value copy = var.deepcopy(); + copy.boundVariable = var.boundVariable; + globalVariables.put(key, (c, t) -> copy); + }); + } + + public void setImportsBasedOn(ScriptHost host, ModuleData other) + { + // fixing imports + other.functionImports.forEach((name, targetData) -> { + functionImports.put(name, host.moduleData.get(targetData.parent)); + }); + other.globalsImports.forEach((name, targetData) -> { + globalsImports.put(name, host.moduleData.get(targetData.parent)); + }); + other.futureImports.forEach((name, targetData) -> { + futureImports.put(name, host.moduleData.get(targetData.parent)); + }); + + } + + public ModuleData(Module parent) + { + this.parent = parent; + } + } + + protected final Map userHosts = new Object2ObjectOpenHashMap<>(); + private final Map moduleData = new HashMap<>(); // marking imports + private final Map modules = new HashMap<>(); + + protected ScriptHost parent; + protected boolean perUser; + public String user; + + public String getName() + { + return main == null ? null : main.name(); + } + + public String getVisualName() + { + return main == null ? "built-in default app" : main.name(); + } + + public boolean isDefaultApp() + { + return main == null; + } + + @Nullable + public final Module main; + + @FunctionalInterface + public interface ErrorSnooper + { + List apply(Expression expression, Tokenizer.Token token, Context context, String message); + } + + public ErrorSnooper errorSnooper = null; + + protected ScriptHost(@Nullable Module code, ScriptServer scriptServer, boolean perUser, ScriptHost parent) + { + this.parent = parent; + this.main = code; + this.perUser = perUser; + this.user = null; + this.strict = false; + this.scriptServer = scriptServer; + ModuleData moduleData = new ModuleData(code); + initializeModuleGlobals(moduleData); + this.moduleData.put(code, moduleData); + this.modules.put(code == null ? null : code.name(), code); + mainThread = Thread.currentThread(); + } + + void initializeModuleGlobals(ModuleData md) + { + } + + public void importModule(Context c, String moduleName) + { + if (modules.containsKey(moduleName.toLowerCase(Locale.ROOT))) + { + return; // aready imported + } + Module module = getModuleOrLibraryByName(moduleName); + if (modules.containsKey(module.name())) + { + return; // aready imported, once again, in case some discrepancies in names? + } + modules.put(module.name(), module); + ModuleData data = new ModuleData(module); + initializeModuleGlobals(data); + moduleData.put(module, data); + runModuleCode(c, module); + //moduleData.remove(module); // we are pooped already, but doesn't hurt to clean that up. + //modules.remove(module.getName()); + //throw new InternalExpressionException("Failed to import a module "+moduleName); + } + + public void importNames(Context c, Module targetModule, String sourceModuleName, List identifiers) + { + if (!moduleData.containsKey(targetModule)) + { + throw new InternalExpressionException("Cannot import to module that doesn't exist"); + } + Module source = modules.get(sourceModuleName); + ModuleData sourceData = moduleData.get(source); + ModuleData targetData = moduleData.get(targetModule); + if (sourceData == null || targetData == null) + { + throw new InternalExpressionException("Cannot import from module that is not imported"); + } + for (String identifier : identifiers) + { + if (sourceData.globalFunctions.containsKey(identifier)) + { + targetData.functionImports.put(identifier, sourceData); + } + else if (sourceData.globalVariables.containsKey(identifier)) + { + targetData.globalsImports.put(identifier, sourceData); + } + else + { + targetData.futureImports.put(identifier, sourceData); + } + } + } + + public Stream availableImports(String moduleName) + { + Module source = modules.get(moduleName); + ModuleData sourceData = moduleData.get(source); + if (sourceData == null) + { + throw new InternalExpressionException("Cannot import from module that is not imported"); + } + return Stream.concat( + globalVariableNames(source, s -> s.startsWith("global_")), + globalFunctionNames(source, s -> true) + ).distinct().sorted(); + } + + protected abstract Module getModuleOrLibraryByName(String name); // this should be shell out in the executor + + protected abstract void runModuleCode(Context c, Module module); // this should be shell out in the executor + + public FunctionValue getFunction(String name) + { + return getFunction(main, name); + } + + public FunctionValue getAssertFunction(Module module, String name) + { + FunctionValue ret = getFunction(module, name); + if (ret == null) + { + if (module == main) + { + throw new InternalExpressionException("Function '" + name + "' is not defined yet"); + } + else + { + throw new InternalExpressionException("Function '" + name + "' is not defined nor visible by its name in the imported module '" + module.name() + "'"); + } + } + return ret; + } + + private FunctionValue getFunction(Module module, String name) + { + ModuleData local = getModuleData(module); + FunctionValue ret = local.globalFunctions.get(name); // most uses would be from local scope anyways + if (ret != null) + { + return ret; + } + ModuleData target = local.functionImports.get(name); + if (target != null) + { + ret = target.globalFunctions.get(name); + if (ret != null) + { + return ret; + } + } + // not in local scope - will need to travel over import links + target = local.futureImports.get(name); + if (target == null) + { + return null; + } + target = findModuleDataFromFunctionImports(name, target, 0); + if (target == null) + { + return null; + } + local.futureImports.remove(name); + local.functionImports.put(name, target); + return target.globalFunctions.get(name); + } + + private ModuleData findModuleDataFromFunctionImports(String name, ModuleData source, int ttl) + { + if (ttl > 64) + { + throw new InternalExpressionException("Cannot import " + name + ", either your imports are too deep or too loopy"); + } + if (source.globalFunctions.containsKey(name)) + { + return source; + } + if (source.functionImports.containsKey(name)) + { + return findModuleDataFromFunctionImports(name, source.functionImports.get(name), ttl + 1); + } + if (source.futureImports.containsKey(name)) + { + return findModuleDataFromFunctionImports(name, source.futureImports.get(name), ttl + 1); + } + return null; + } + + public LazyValue getGlobalVariable(String name) + { + return getGlobalVariable(main, name); + } + + public LazyValue getGlobalVariable(Module module, String name) + { + ModuleData local = getModuleData(module); + LazyValue ret = local.globalVariables.get(name); // most uses would be from local scope anyways + if (ret != null) + { + return ret; + } + ModuleData target = local.globalsImports.get(name); + if (target != null) + { + ret = target.globalVariables.get(name); + if (ret != null) + { + return ret; + } + } + // not in local scope - will need to travel over import links + target = local.futureImports.get(name); + if (target == null) + { + return null; + } + target = findModuleDataFromGlobalImports(name, target, 0); + if (target == null) + { + return null; + } + local.futureImports.remove(name); + local.globalsImports.put(name, target); + return target.globalVariables.get(name); + } + + private ModuleData findModuleDataFromGlobalImports(String name, ModuleData source, int ttl) + { + if (ttl > 64) + { + throw new InternalExpressionException("Cannot import " + name + ", either your imports are too deep or too loopy"); + } + if (source.globalVariables.containsKey(name)) + { + return source; + } + if (source.globalsImports.containsKey(name)) + { + return findModuleDataFromGlobalImports(name, source.globalsImports.get(name), ttl + 1); + } + if (source.futureImports.containsKey(name)) + { + return findModuleDataFromGlobalImports(name, source.futureImports.get(name), ttl + 1); + } + return null; + } + + public void delFunctionWithPrefix(Module module, String prefix) + { + ModuleData data = getModuleData(module); + data.globalFunctions.entrySet().removeIf(e -> e.getKey().startsWith(prefix)); + data.functionImports.entrySet().removeIf(e -> e.getKey().startsWith(prefix)); + } + + public void delFunction(Module module, String funName) + { + ModuleData data = getModuleData(module); + data.globalFunctions.remove(funName); + data.functionImports.remove(funName); + } + + public void delGlobalVariableWithPrefix(Module module, String prefix) + { + ModuleData data = getModuleData(module); + data.globalVariables.entrySet().removeIf(e -> e.getKey().startsWith(prefix)); + data.globalsImports.entrySet().removeIf(e -> e.getKey().startsWith(prefix)); + } + + public void delGlobalVariable(Module module, String varName) + { + ModuleData data = getModuleData(module); + data.globalFunctions.remove(varName); + data.functionImports.remove(varName); + } + + private ModuleData getModuleData(Module module) + { + ModuleData data = moduleData.get(module); + if (data == null) + { + throw new IntegrityException("Module structure changed for the app. Did you reload the app with tasks running?"); + } + return data; + } + + protected void assertAppIntegrity(Module module) + { + getModuleData(module); + } + + public void addUserDefinedFunction(Context ctx, Module module, String name, FunctionValue fun) + { + getModuleData(module).globalFunctions.put(name, fun); + } + + public void setGlobalVariable(Module module, String name, LazyValue lv) + { + getModuleData(module).globalVariables.put(name, lv); + } + + public Stream globalVariableNames(Module module, Predicate predicate) + { + return Stream.concat(Stream.concat( + getModuleData(module).globalVariables.keySet().stream(), + getModuleData(module).globalsImports.keySet().stream() + ), getModuleData(module).futureImports.keySet().stream().filter(s -> s.startsWith("global_"))).filter(predicate); + } + + public Stream globalFunctionNames(Module module, Predicate predicate) + { + return Stream.concat(Stream.concat( + getModuleData(module).globalFunctions.keySet().stream(), + getModuleData(module).functionImports.keySet().stream() + ), getModuleData(module).futureImports.keySet().stream().filter(s -> !s.startsWith("global_"))).filter(predicate); + } + + public ScriptHost retrieveForExecution(String /*Nullable*/ user) + { + if (!perUser) + { + return this; + } + ScriptHost oldUserHost = userHosts.get(user); + if (oldUserHost != null) + { + return oldUserHost; + } + ScriptHost userHost = this.duplicate(); + userHost.user = user; + this.setupUserHost(userHost); + userHosts.put(user, userHost); + return userHost; + } + + protected void setupUserHost(ScriptHost host) + { + // adding imports + host.modules.putAll(this.modules); + this.moduleData.forEach((key, value) -> host.moduleData.put(key, new ModuleData(key, value))); + // fixing imports + host.moduleData.forEach((module, data) -> data.setImportsBasedOn(host, this.moduleData.get(data.parent))); + } + + public synchronized void handleExpressionException(String msg, ExpressionException exc) + { + System.out.println(msg + ": " + exc); + } + + protected abstract ScriptHost duplicate(); + + public Object getLock(Value name) + { + return locks.computeIfAbsent(name, n -> new Object()); + } + + public ThreadPoolExecutor getExecutor(Value pool) + { + if (inTermination) + { + return null; + } + return executorServices.computeIfAbsent(pool, v -> (ThreadPoolExecutor) Executors.newCachedThreadPool()); + } + + public int taskCount() + { + return executorServices.values().stream().map(ThreadPoolExecutor::getActiveCount).reduce(0, Integer::sum); + } + + public int taskCount(Value pool) + { + return executorServices.containsKey(pool) ? executorServices.get(pool).getActiveCount() : 0; + } + + public void onClose() + { + inTermination = true; + executorServices.values().forEach(ThreadPoolExecutor::shutdown); + for (ScriptHost uh : userHosts.values()) + { + uh.onClose(); + } + if (taskCount() > 0) + { + executorServices.values().forEach(e -> { + ExecutorService stopper = Executors.newSingleThreadExecutor(); + stopper.submit(() -> { + try + { + // Wait a while for existing tasks to terminate + if (!e.awaitTermination(1500, TimeUnit.MILLISECONDS)) + { + e.shutdownNow(); // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!e.awaitTermination(1500, TimeUnit.MILLISECONDS)) + { + CarpetScriptServer.LOG.error("Failed to stop app's thread"); + } + } + } + catch (InterruptedException ie) + { + // (Re-)Cancel if current thread also interrupted + e.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + stopper.shutdown(); + stopper.shutdownNow(); + }); + }); + } + } + + public void setPerPlayer(boolean isPerUser) + { + perUser = isPerUser; + } + + public boolean isPerUser() + { + return perUser; + } + + public Set getUserList() + { + return userHosts.keySet(); + } + + + public void resetErrorSnooper() + { + errorSnooper = null; + } + + public static final Logger DEPRECATION_LOG = LoggerFactory.getLogger("Scarpet Deprecation Warnings"); + + public boolean issueDeprecation(String feature) + { + if (deprecations.contains(feature)) + { + return false; + } + deprecations.add(feature); + DEPRECATION_LOG.warn("App '" + getVisualName() + "' uses '" + feature + "', which is deprecated for removal. Check the docs for a replacement"); + return true; + } + + public ScriptServer scriptServer() + { + return scriptServer; + } +} diff --git a/src/main/java/carpet/script/ScriptServer.java b/src/main/java/carpet/script/ScriptServer.java new file mode 100644 index 0000000..a00a7f5 --- /dev/null +++ b/src/main/java/carpet/script/ScriptServer.java @@ -0,0 +1,15 @@ +package carpet.script; + +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import carpet.script.value.Value; + +// WIP +public abstract class ScriptServer +{ + public final Map systemGlobals = new ConcurrentHashMap<>(); + + public abstract Path resolveResource(String suffix); +} diff --git a/src/main/java/carpet/script/Tokenizer.java b/src/main/java/carpet/script/Tokenizer.java new file mode 100644 index 0000000..90994f0 --- /dev/null +++ b/src/main/java/carpet/script/Tokenizer.java @@ -0,0 +1,503 @@ +package carpet.script; + +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Expression tokenizer that allows to iterate over a {@link String} + * expression token by token. Blank characters will be skipped. + */ +public class Tokenizer implements Iterator +{ + /** + * What character to use for decimal separators. + */ + private static final char decimalSeparator = '.'; + /** + * What character to use for minus sign (negative values). + */ + private static final char minusSign = '-'; + /** + * Actual position in expression string. + */ + private int pos = 0; + private int lineno = 0; + private int linepos = 0; + private final boolean comments; + private final boolean newLinesMarkers; + /** + * The original input expression. + */ + private final String input; + /** + * The previous token or null if none. + */ + private Token previousToken; + + private final Expression expression; + private final Context context; + + Tokenizer(Context c, Expression expr, String input, boolean allowComments, boolean allowNewLineMakers) + { + this.input = input; + this.expression = expr; + this.context = c; + this.comments = allowComments; + this.newLinesMarkers = allowNewLineMakers; + } + + public List postProcess() + { + Iterable iterable = () -> this; + List originalTokens = StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); + List cleanedTokens = new ArrayList<>(); + Token last = null; + while (!originalTokens.isEmpty()) + { + Token current = originalTokens.remove(originalTokens.size() - 1); + if (current.type == Token.TokenType.MARKER && current.surface.startsWith("//")) + { + continue; + } + // skipping comments + if (!isSemicolon(current) + || (last != null && last.type != Token.TokenType.CLOSE_PAREN && last.type != Token.TokenType.COMMA && !isSemicolon(last))) + { + if (isSemicolon(current)) + { + current.surface = ";"; + current.type = Token.TokenType.OPERATOR; + } + if (current.type == Token.TokenType.MARKER) + { + // dealing with tokens in reversed order + if ("{".equals(current.surface)) + { + cleanedTokens.add(current.morphedInto(Token.TokenType.OPEN_PAREN, "(")); + current.morph(Token.TokenType.FUNCTION, "m"); + } + else if ("[".equals(current.surface)) + { + cleanedTokens.add(current.morphedInto(Token.TokenType.OPEN_PAREN, "(")); + current.morph(Token.TokenType.FUNCTION, "l"); + } + else if ("}".equals(current.surface) || "]".equals(current.surface)) + { + current.morph(Token.TokenType.CLOSE_PAREN, ")"); + } + } + cleanedTokens.add(current); + } + if (!(current.type == Token.TokenType.MARKER && current.surface.equals("$"))) + { + last = current; + } + } + Collections.reverse(cleanedTokens); + return cleanedTokens; + } + + @Override + public boolean hasNext() + { + return (pos < input.length()); + } + + /** + * Peek at the next character, without advancing the iterator. + * + * @return The next character or character 0, if at end of string. + */ + private char peekNextChar() + { + return (pos < (input.length() - 1)) ? input.charAt(pos + 1) : 0; + } + + private boolean isHexDigit(char ch) + { + return ch == 'x' || ch == 'X' || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') + || (ch >= 'A' && ch <= 'F'); + } + + private static boolean isSemicolon(Token tok) + { + return (tok.type == Token.TokenType.OPERATOR && tok.surface.equals(";")) + || (tok.type == Token.TokenType.UNARY_OPERATOR && tok.surface.equals(";u")); + } + + public static List simplepass(String input) + { + Tokenizer tok = new Tokenizer(null, null, input, false, false); + List res = new ArrayList<>(); + while (tok.hasNext()) + { + res.add(tok.next()); + } + return res; + } + + @Override + public Token next() + { + Token token = new Token(); + + if (pos >= input.length()) + { + return previousToken = null; + } + char ch = input.charAt(pos); + while (Character.isWhitespace(ch) && pos < input.length()) + { + linepos++; + if (ch == '\n') + { + lineno++; + linepos = 0; + } + ch = input.charAt(++pos); + } + token.pos = pos; + token.lineno = lineno; + token.linepos = linepos; + + boolean isHex = false; + + if (Character.isDigit(ch)) // || (ch == decimalSeparator && Character.isDigit(peekNextChar()))) + // decided to no support this notation to favour element access via . operator + { + if (ch == '0' && (peekNextChar() == 'x' || peekNextChar() == 'X')) + { + isHex = true; + } + while ((isHex + && isHexDigit( + ch)) + || (Character.isDigit(ch) || ch == decimalSeparator || ch == 'e' || ch == 'E' + || (ch == minusSign && token.length() > 0 + && ('e' == token.charAt(token.length() - 1) + || 'E' == token.charAt(token.length() - 1))) + || (ch == '+' && token.length() > 0 + && ('e' == token.charAt(token.length() - 1) + || 'E' == token.charAt(token.length() - 1)))) + && (pos < input.length())) + { + token.append(input.charAt(pos++)); + linepos++; + ch = pos == input.length() ? 0 : input.charAt(pos); + } + token.type = isHex ? Token.TokenType.HEX_LITERAL : Token.TokenType.LITERAL; + } + else if (ch == '\'') + { + pos++; + linepos++; + token.type = Token.TokenType.STRINGPARAM; + if (pos == input.length() && expression != null && context != null) + { + throw new ExpressionException(context, this.expression, token, "Program truncated"); + } + ch = input.charAt(pos); + while (ch != '\'') + { + if (ch == '\\') + { + char nextChar = peekNextChar(); + if (nextChar == 'n') + { + token.append('\n'); + } + else if (nextChar == 't') + { + //throw new ExpressionException(context, this.expression, token, + // "Tab character is not supported"); + token.append('\t'); + } + else if (nextChar == 'r') + { + throw new ExpressionException(context, this.expression, token, + "Carriage return character is not supported"); + //token.append('\r'); + } + else if (nextChar == '\\' || nextChar == '\'') + { + token.append(nextChar); + } + else + { + pos--; + linepos--; + } + pos += 2; + linepos += 2; + if (pos == input.length() && expression != null && context != null) + { + throw new ExpressionException(context, this.expression, token, "Program truncated"); + } + } + else + { + 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"); + } + } + ch = input.charAt(pos); + } + pos++; + linepos++; + + } + else if (Character.isLetter(ch) || "_".indexOf(ch) >= 0) + { + while ((Character.isLetter(ch) || Character.isDigit(ch) || "_".indexOf(ch) >= 0 + || token.length() == 0 && "_".indexOf(ch) >= 0) && (pos < input.length())) + { + token.append(input.charAt(pos++)); + linepos++; + ch = pos == input.length() ? 0 : input.charAt(pos); + } + // Remove optional white spaces after function or variable name + if (Character.isWhitespace(ch)) + { + while (Character.isWhitespace(ch) && pos < input.length()) + { + ch = input.charAt(pos++); + linepos++; + if (ch == '\n') + { + lineno++; + linepos = 0; + } + } + pos--; + linepos--; + } + token.type = ch == '(' ? Token.TokenType.FUNCTION : Token.TokenType.VARIABLE; + } + else if (ch == '(' || ch == ')' || ch == ',' || + ch == '{' || ch == '}' || ch == '[' || ch == ']') + { + if (ch == '(') + { + token.type = Token.TokenType.OPEN_PAREN; + } + else if (ch == ')') + { + token.type = Token.TokenType.CLOSE_PAREN; + } + else if (ch == ',') + { + token.type = Token.TokenType.COMMA; + } + else + { + token.type = Token.TokenType.MARKER; + } + token.append(ch); + pos++; + linepos++; + + if (expression != null && context != null && previousToken != null && + previousToken.type == Token.TokenType.OPERATOR && + (ch == ')' || ch == ',' || ch == ']' || ch == '}') && + !previousToken.surface.equalsIgnoreCase(";") + ) + { + throw new ExpressionException(context, this.expression, previousToken, + "Can't have operator " + previousToken.surface + " at the end of a subexpression"); + } + } + else + { + String greedyMatch = ""; + int initialPos = pos; + int initialLinePos = linepos; + ch = input.charAt(pos); + int validOperatorSeenUntil = -1; + while (!Character.isLetter(ch) && !Character.isDigit(ch) && "_".indexOf(ch) < 0 + && !Character.isWhitespace(ch) && ch != '(' && ch != ')' && ch != ',' + && (pos < input.length())) + { + greedyMatch += ch; + if (comments && "//".equals(greedyMatch)) + { + + while (ch != '\n' && pos < input.length()) + { + ch = input.charAt(pos++); + linepos++; + greedyMatch += ch; + } + if (ch == '\n') + { + lineno++; + linepos = 0; + } + token.append(greedyMatch); + token.type = Token.TokenType.MARKER; + return token; // skipping setting previous + } + pos++; + linepos++; + if (Expression.none.isAnOperator(greedyMatch)) + { + validOperatorSeenUntil = pos; + } + ch = pos == input.length() ? 0 : input.charAt(pos); + } + if (newLinesMarkers && "$".equals(greedyMatch)) + { + lineno++; + linepos = 0; + token.type = Token.TokenType.MARKER; + token.append('$'); + return token; // skipping previous token lookback + } + if (validOperatorSeenUntil != -1) + { + token.append(input.substring(initialPos, validOperatorSeenUntil)); + pos = validOperatorSeenUntil; + linepos = initialLinePos + validOperatorSeenUntil - initialPos; + } + else + { + token.append(greedyMatch); + } + + if (previousToken == null || previousToken.type == Token.TokenType.OPERATOR + || previousToken.type == Token.TokenType.OPEN_PAREN || previousToken.type == Token.TokenType.COMMA + || (previousToken.type == Token.TokenType.MARKER && (previousToken.surface.equals("{") || previousToken.surface.equals("["))) + ) + { + token.surface += "u"; + token.type = Token.TokenType.UNARY_OPERATOR; + } + else + { + token.type = Token.TokenType.OPERATOR; + } + } + if (expression != null && context != null && previousToken != null && + ( + token.type == Token.TokenType.LITERAL || + token.type == Token.TokenType.HEX_LITERAL || + token.type == Token.TokenType.VARIABLE || + token.type == Token.TokenType.STRINGPARAM || + (token.type == Token.TokenType.MARKER && (previousToken.surface.equalsIgnoreCase("{") || previousToken.surface.equalsIgnoreCase("["))) || + token.type == Token.TokenType.FUNCTION + ) && ( + previousToken.type == Token.TokenType.VARIABLE || + previousToken.type == Token.TokenType.FUNCTION || + previousToken.type == Token.TokenType.LITERAL || + previousToken.type == Token.TokenType.CLOSE_PAREN || + (previousToken.type == Token.TokenType.MARKER && (previousToken.surface.equalsIgnoreCase("}") || previousToken.surface.equalsIgnoreCase("]"))) || + previousToken.type == Token.TokenType.HEX_LITERAL || + previousToken.type == Token.TokenType.STRINGPARAM + ) + ) + { + throw new ExpressionException(context, this.expression, previousToken, "'" + token.surface + "' is not allowed after '" + previousToken.surface + "'"); + } + return previousToken = token; + } + + @Override + public void remove() + { + throw new InternalExpressionException("remove() not supported"); + } + + public static class Token + { + enum TokenType + { + FUNCTION(true, false), OPERATOR(true, false), UNARY_OPERATOR(true, false), + VARIABLE(false, false), CONSTANT(false, true), + LITERAL(false, true), HEX_LITERAL(false, true), STRINGPARAM(false, true), + OPEN_PAREN(false, true), COMMA(false, true), CLOSE_PAREN(false, true), MARKER(false, true); + + final boolean functional; + final boolean constant; + + TokenType(boolean functional, boolean constant) + { + this.functional = functional; + this.constant = constant; + } + + public boolean isFunctional() + { + return functional; + } + + public boolean isConstant() + { + return constant; + } + } + + public String surface = ""; + public TokenType type; + public int pos; + public int linepos; + public int lineno; + public static final Token NONE = new Token(); + + public Token morphedInto(TokenType newType, String newSurface) + { + Token created = new Token(); + created.surface = newSurface; + created.type = newType; + created.pos = pos; + created.linepos = linepos; + created.lineno = lineno; + return created; + } + + public void morph(TokenType type, String s) + { + this.type = type; + this.surface = s; + } + + public void append(char c) + { + surface += c; + } + + public void append(String s) + { + surface += s; + } + + public char charAt(int pos) + { + return surface.charAt(pos); + } + + public int length() + { + return surface.length(); + } + + @Override + public String toString() + { + return surface; + } + } +} diff --git a/src/main/java/carpet/script/annotation/AnnotationParser.java b/src/main/java/carpet/script/annotation/AnnotationParser.java new file mode 100644 index 0000000..d0648cd --- /dev/null +++ b/src/main/java/carpet/script/annotation/AnnotationParser.java @@ -0,0 +1,359 @@ +package carpet.script.annotation; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import carpet.script.CarpetContext; +import net.minecraft.core.RegistryAccess; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.ClassUtils; + +import com.google.common.base.Suppliers; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Fluff.AbstractLazyFunction; +import carpet.script.Fluff.TriFunction; +import carpet.script.Fluff.UsageProvider; +import carpet.script.exception.InternalExpressionException; +import carpet.script.LazyValue; +import carpet.script.value.Value; + +/** + *

This class parses methods annotated with the {@link ScarpetFunction} annotation in a given {@link Class}, generating + * fully-featured, automatically parsed and converted functions to be used in the Scarpet language.

+ * + *

This class and the rest in this package will try to ensure that the annotated method receives the proper parameters + * directly, without all the always-repeated code of evaluating {@link Value}s, checking and casting them to their respective + * types, and converting them to the final needed object.

+ * + *

To do that, functions will save a list of {@link ValueConverter}s to convert all their parameters. {@link ValueConverter}s + * are able to convert from any compatible {@link Value} instance into the requested parameter type, as long as they are registered + * using their respective {@code register} functions.

+ * + *

Built-in {@link ValueConverter}s include but are not limited to converters to convert {@link List}s to actual Java lists while also + * converting every item inside of the {@link List} to the specified generic parameter ({@code <>}), with the same applying for maps

+ * + *

Parameters can be given the annotations (present in the {@link Locator} and {@link Param} interfaces) in order to restrict them or + * make them more permissive to accept types, such as {@link Param.AllowSingleton} for lists.

+ * + *

You can also declare optional parameters by using Java's {@link Optional} as the type of one of your parameters, though it must be + * at the end of the function, just before varargs if present and/or any other {@link Optional} parameters.

+ * + *

Output of the annotated methods will also be converted to a compatible {@link LazyValue} using the registered {@link OutputConverter}s, + * allowing to remove the need of explicitly converting to a {@link Value} and then to a {@link LazyValue} just to end the method (though you can + * return a {@link LazyValue} if you want.

+ * + *

For a variable argument count, the Java varargs notation can be used in the last parameter, converting the function into a variable argument + * function that will pass all the rest of parameters to that last varargs parameter, also converted into the specified type.

+ * + *

To begin, use the {@link #parseFunctionClass(Class)} method in a class with methods annotated with the {@link ScarpetFunction} annotation.

+ * + * @see ScarpetFunction + * @see Locator.Block + * @see Locator.Vec3d + * @see Param.Strict + * @see Param.AllowSingleton + * @see Param.KeyValuePairs + * @see Param.Custom + * @see Optional + * @see OutputConverter#registerToValue(Class, java.util.function.Function) + * @see OutputConverter#register(Class, java.util.function.Function) + * @see ValueCaster#register(Class, String) + * @see SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String) + * @see Param.Params#registerStrictConverter(Class, boolean, ValueConverter) + * @see Param.Params#registerCustomConverterFactory(java.util.function.BiFunction) + */ +public final class AnnotationParser +{ + static final int UNDEFINED_PARAMS = -2; + static final String USE_METHOD_NAME = "$METHOD_NAME_MARKER$"; + private static final List functionList = new ArrayList<>(); + + /** + *

Parses a given {@link Class} and registers its annotated methods, the ones with the {@link ScarpetFunction} annotation, + * to be used in the Scarpet language.

+ * + *

Only call this method once per class per lifetime of the JVM! (for example, at {@link carpet.CarpetExtension#onGameStarted()} or + * {@link net.fabricmc.api.ModInitializer#onInitialize()}).

+ * + *

There is a set of requirements for the class and its methods:

+ *
    + *
  • Annotated methods must not throw checked exceptions. They can throw regular {@link RuntimeException}s (including but not limited to + * {@link InternalExpressionException}). + * Basically, it's fine as long as you don't add a {@code throws} declaration to your methods.
  • + *
  • Varargs (or effectively varargs) annotated methods must explicitly declare a maximum number of parameters to ingest in the {@link ScarpetFunction} + * annotation. They can still declare an unlimited amount by setting that maximum to {@link ScarpetFunction#UNLIMITED_PARAMS}. "Effectively varargs" + * means a function that has at least a parameter requiring a {@link ValueConverter} that has declared {@link ValueConverter#consumesVariableArgs()}.
  • + *
  • Annotated methods must not have a parameter with generics as the varargs parameter. This is just because it was painful for me (altrisi) and + * didn't want to support it. Those will crash with a {@code ClassCastException}
  • + *
+ *

Additionally, if the class contains annotated instance (non-static) methods, the class must be concrete and provide a no-arg constructor + * to instantiate it.

+ * + * @param clazz The class to parse + * @see ScarpetFunction + */ + public static void parseFunctionClass(Class clazz) + { + // Only try to instantiate or require concrete classes if there are non-static annotated methods + Supplier instanceSupplier = Suppliers.memoize(() -> { + if (Modifier.isAbstract(clazz.getModifiers())) + { + throw new IllegalArgumentException("Function class must be concrete to support non-static methods! Class: " + clazz.getSimpleName()); + } + try + { + return clazz.getConstructor().newInstance(); + } + catch (ReflectiveOperationException e) + { + throw new IllegalArgumentException( + "Couldn't create instance of given " + clazz + ". This is needed for non-static methods. Make sure default constructor is available", e); + } + }); + Method[] methodz = clazz.getDeclaredMethods(); + for (Method method : methodz) + { + if (!method.isAnnotationPresent(ScarpetFunction.class)) + { + continue; + } + + if (method.getExceptionTypes().length != 0) + { + throw new IllegalArgumentException("Annotated method '" + method.getName() + "', provided in '" + clazz + "' must not declare checked exceptions"); + } + + ParsedFunction function = new ParsedFunction(method, clazz, instanceSupplier); + functionList.add(function); + } + } + + /** + *

Adds all parsed functions to the given {@link Expression}.

+ *

This is handled automatically by Carpet

+ * + * @param expr The expression to add every function to + */ + public static void apply(Expression expr) + { + for (ParsedFunction function : functionList) + { + expr.addLazyFunction(function.name, function.scarpetParamCount, function); + } + } + + private static class ParsedFunction implements TriFunction, LazyValue>, UsageProvider + { + private final String name; + private final boolean isMethodVarArgs; + private final int methodParamCount; + private final ValueConverter[] valueConverters; + private final Class varArgsType; + private final boolean primitiveVarArgs; + private final ValueConverter varArgsConverter; + private final OutputConverter outputConverter; + private final boolean isEffectivelyVarArgs; + private final int minParams; + private final int maxParams; + private final MethodHandle handle; + private final int scarpetParamCount; + private final Context.Type contextType; + + private ParsedFunction(Method method, Class originClass, Supplier instance) + { + ScarpetFunction annotation = method.getAnnotation(ScarpetFunction.class); + this.name = USE_METHOD_NAME.equals(annotation.functionName()) ? method.getName() : annotation.functionName(); + this.isMethodVarArgs = method.isVarArgs(); + this.methodParamCount = method.getParameterCount(); + + Parameter[] methodParameters = method.getParameters(); + this.valueConverters = new ValueConverter[isMethodVarArgs ? methodParamCount - 1 : methodParamCount]; + for (int i = 0; i < this.methodParamCount; i++) + { + Parameter param = methodParameters[i]; + if (!isMethodVarArgs || i != this.methodParamCount - 1) // Varargs converter is separate + { + this.valueConverters[i] = ValueConverter.fromAnnotatedType(param.getAnnotatedType()); + } + } + Class originalVarArgsType = isMethodVarArgs ? methodParameters[methodParamCount - 1].getType().getComponentType() : null; + this.varArgsType = ClassUtils.primitiveToWrapper(originalVarArgsType); // Primitive array cannot be cast to Obj[] + this.primitiveVarArgs = originalVarArgsType != null && originalVarArgsType.isPrimitive(); + this.varArgsConverter = isMethodVarArgs ? ValueConverter.fromAnnotatedType(methodParameters[methodParamCount - 1].getAnnotatedType()) : null; + @SuppressWarnings("unchecked") // Yes. Making a T is not worth + OutputConverter converter = OutputConverter.get((Class) method.getReturnType()); + this.outputConverter = converter; + + this.isEffectivelyVarArgs = isMethodVarArgs || Arrays.stream(valueConverters).anyMatch(ValueConverter::consumesVariableArgs); + this.minParams = Arrays.stream(valueConverters).mapToInt(ValueConverter::valueConsumption).sum(); // Note: In !varargs, this is params + int setMaxParams = this.minParams; // Unlimited == Integer.MAX_VALUE + if (this.isEffectivelyVarArgs) + { + setMaxParams = annotation.maxParams(); + if (setMaxParams == UNDEFINED_PARAMS) + { + throw new IllegalArgumentException("No maximum number of params specified for " + name + ", use ScarpetFunction.UNLIMITED_PARAMS for unlimited. " + + "Provided in " + originClass); + } + if (setMaxParams == ScarpetFunction.UNLIMITED_PARAMS) + { + setMaxParams = Integer.MAX_VALUE; + } + if (setMaxParams < this.minParams) + { + throw new IllegalArgumentException("Provided maximum number of params for " + name + " is smaller than method's param count." + + "Provided in " + originClass); + } + } + this.maxParams = setMaxParams; + + // Why MethodHandles? + // MethodHandles are blazing fast (in some situations they can even compile to a single invokeVirtual), but slightly complex to work with. + // Their "polymorphic signature" makes them (by default) require the exact signature of the method and return type, in order to call the + // functions directly, not even accepting Objects. Therefore we change them to spreaders (accept array instead), return type of Object, + // and we also bind them to our instance. That makes them ever-so-slightly slower, since they have to cast the params, but it's not + // noticeable, and comparing the overhead that reflection's #invoke had over this, the difference is substantial + // (checking access at invoke vs create). + // Note: there is also MethodHandle#invoke and #invokeWithArguments, but those run #asType at every invocation, which is quite slow, so we + // are basically running it here. + try + { + MethodHandle tempHandle = MethodHandles.publicLookup().unreflect(method).asFixedArity().asSpreader(Object[].class, this.methodParamCount); + tempHandle = tempHandle.asType(tempHandle.type().changeReturnType(Object.class)); + this.handle = Modifier.isStatic(method.getModifiers()) ? tempHandle : tempHandle.bindTo(instance.get()); + } + catch (IllegalAccessException e) + { + throw new IllegalArgumentException(e); + } + + this.scarpetParamCount = this.isEffectivelyVarArgs ? -1 : this.minParams; + this.contextType = annotation.contextType(); + } + + @Override + public LazyValue apply(Context context, Context.Type t, List lazyValues) + { + // yes we are making a minecraft dependency, because of the stupid registry access required to parse stuff + RegistryAccess regs = ((CarpetContext) context).registryAccess(); + List lv = AbstractLazyFunction.unpackLazy(lazyValues, context, contextType); + if (isEffectivelyVarArgs) + { + if (lv.size() < minParams) + { + throw new InternalExpressionException("Function '" + name + "' expected at least " + minParams + " arguments, got " + lv.size() + ". " + + getUsage()); + } + if (lv.size() > maxParams) + { + throw new InternalExpressionException("Function '" + name + " expected up to " + maxParams + " arguments, got " + lv.size() + ". " + + getUsage()); + } + } + Object[] params = getMethodParams(lv, context, t); + try + { + Value result = outputConverter.convert(handle.invokeExact(params), regs); + return (cc, tt) -> result; + } + catch (Throwable e) + { + if (e instanceof RuntimeException re) + { + throw re; + } + throw (Error) e; // Stack overflow or something. Methods are guaranteed not to throw checked exceptions + } + } + + // Hot code: Must be optimized + private Object[] getMethodParams(List lv, Context context, Context.Type theLazyT) + { + Object[] params = new Object[methodParamCount]; + ListIterator lvIterator = lv.listIterator(); + + int regularArgs = isMethodVarArgs ? methodParamCount - 1 : methodParamCount; + for (int i = 0; i < regularArgs; i++) + { + params[i] = valueConverters[i].checkAndConvert(lvIterator, context, theLazyT); + if (params[i] == null) + { + throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage()); + } + } + if (isMethodVarArgs) + { + int remaining = lv.size() - lvIterator.nextIndex(); + Object[] varArgs; + if (varArgsConverter.consumesVariableArgs()) + { + List varArgsList = new ArrayList<>(); // fastutil's is extremely slow in toArray, and we use that + while (lvIterator.hasNext()) + { + Object obj = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT); + if (obj == null) + { + throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage()); + } + varArgsList.add(obj); + } + varArgs = varArgsList.toArray((Object[]) Array.newInstance(varArgsType, 0)); + } + else + { + varArgs = (Object[]) Array.newInstance(varArgsType, remaining / varArgsConverter.valueConsumption()); + for (int i = 0; lvIterator.hasNext(); i++) + { + varArgs[i] = varArgsConverter.checkAndConvert(lvIterator, context, theLazyT); + if (varArgs[i] == null) + { + throw new InternalExpressionException("Incorrect argument passsed to '" + name + "' function.\n" + getUsage()); + } + } + } + params[methodParamCount - 1] = primitiveVarArgs ? ArrayUtils.toPrimitive(varArgs) : varArgs; // Copies the array + } + return params; + } + + @Override + public String getUsage() + { + // Possibility: More descriptive messages using param.getName()? Would need changing gradle setup to keep those + StringBuilder builder = new StringBuilder("Usage: '"); + builder.append(name); + builder.append('('); + builder.append(Arrays.stream(valueConverters).map(ValueConverter::getTypeName).filter(Objects::nonNull).collect(Collectors.joining(", "))); + if (varArgsConverter != null) + { + builder.append(", "); + builder.append(varArgsConverter.getTypeName()); + builder.append("s...)"); + } + else + { + builder.append(')'); + } + builder.append("'"); + return builder.toString(); + } + } + + private AnnotationParser() + { + } +} diff --git a/src/main/java/carpet/script/annotation/ListConverter.java b/src/main/java/carpet/script/annotation/ListConverter.java new file mode 100644 index 0000000..b02d501 --- /dev/null +++ b/src/main/java/carpet/script/annotation/ListConverter.java @@ -0,0 +1,100 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import carpet.script.Context; +import carpet.script.value.ListValue; +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +/** + *

Converts a given {@link ListValue} into a {@link List} of values converted to {@code }.

+ * + *

If the {@link Param.AllowSingleton} annotation is specified, allows creating a singleton from + * a loose element compatible with the type conversion.

+ * + *

Lists provided by this converter are not linked to the initial list, and therefore will not + * reflect changes in either of them

+ * + * @param The type of the element that will be inside the list + */ +final class ListConverter implements ValueConverter> +{ + private final ValueConverter itemConverter; + private final boolean allowSingletonCreation; + + @Override + public String getTypeName() + { + return (allowSingletonCreation ? itemConverter.getTypeName() + " or " : "") + "list of " + itemConverter.getTypeName() + "s"; + } + + @Nullable + @Override + public List convert(Value value, @Nullable Context context) + { + return value instanceof ListValue ? convertListValue((ListValue) value, context) : allowSingletonCreation ? convertSingleton(value, context) : null; + } + + @Nullable + private List convertListValue(ListValue values, @Nullable Context context) + { + List list = new ArrayList<>(values.getItems().size()); + for (Value value : values) + { + T converted = itemConverter.convert(value, context); + if (converted == null) + { + return null; + } + list.add(converted); + } + return list; + } + + @Nullable + private List convertSingleton(Value val, @Nullable Context context) + { + T converted = itemConverter.convert(val, context); + if (converted == null) + { + return null; + } + return Collections.singletonList(converted); + + } + + private ListConverter(AnnotatedType itemType, boolean allowSingletonCreation) + { + itemConverter = ValueConverter.fromAnnotatedType(itemType); + this.allowSingletonCreation = allowSingletonCreation; + } + + /** + *

Returns a new {@link ListConverter} to convert to the given {@link AnnotatedType}.

+ * + *

The returned {@link ValueConverter} will convert the objects inside the list to the + * generics specified in the {@link AnnotatedType}, and the {@link ValueConverter} will + * be set to accept non-list (but correct) items and make a singleton out of them + * if the {@link Param.AllowSingleton} annotation has been specified.

+ * + * @apiNote This method expects the {@link AnnotatedType} to already be of {@link List} type, and, while it will + * technically accept a non-{@link List} {@link AnnotatedType}, it will fail with an {@link ArrayIndexOutOfBoundsException} + * if it doesn't has at least one generic parameter. + * @param annotatedType The type to get generics information from + * @return A new {@link ListConverter} for the data specified in the {@link AnnotatedType} + */ + static ListConverter fromAnnotatedType(AnnotatedType annotatedType) + { + AnnotatedParameterizedType paramType = (AnnotatedParameterizedType) annotatedType; + AnnotatedType itemType = paramType.getAnnotatedActualTypeArguments()[0]; + boolean allowSingletonCreation = annotatedType.isAnnotationPresent(Param.AllowSingleton.class); + return new ListConverter<>(itemType, allowSingletonCreation); + } + +} diff --git a/src/main/java/carpet/script/annotation/Locator.java b/src/main/java/carpet/script/annotation/Locator.java new file mode 100644 index 0000000..af59274 --- /dev/null +++ b/src/main/java/carpet/script/annotation/Locator.java @@ -0,0 +1,304 @@ +package carpet.script.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.util.Iterator; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import com.google.common.collect.Lists; + +import carpet.script.CarpetContext; +import carpet.script.Context; +import carpet.script.argument.Argument; +import carpet.script.argument.BlockArgument; +import carpet.script.argument.FunctionArgument; +import carpet.script.argument.Vector3Argument; +import carpet.script.Module; +import carpet.script.value.BlockValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + *

Class that holds the annotations for {@link Argument} locators, in order for them to be used in Scarpet functions.

+ */ +public interface Locator +{ + /** + *

Represents that the annotated argument must be gotten by passing the arguments in there into a {@link BlockArgument} locator.

+ * + *

Must be used in either {@link BlockArgument}, {@link BlockValue}, {@link BlockPos} or {@link BlockState} parameters

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface Block + { + /** + *

Whether or not should the locator accept a single {@link String} as the parameter and let parsing to {@link BlockValue}.

+ */ + boolean acceptString() default false; + + /** + *

Whether or not should the {@link BlockValue} argument be optional.

Requires the annotation to be present in a + * {@link BlockArgument} type, since it may return that the {@link BlockValue} is {@code null}, which would be considered as an incorrect + * type.

+ */ + boolean optional() default false; + + /** + *

Whether or not should the {@link BlockArgument} locator accept any string as the argument.

Requires the annotation to be present + * in a {@link BlockArgument} type, since it may just return a {@link String}

+ */ + boolean anyString() default false; + } + + /** + *

Represents that the annotated argument must be gotten by passing the arguments in there into a {@link Vector3Argument} locator.

+ * + *

Must be used in either a {@link Vector3Argument} or a {@link net.minecraft.world.phys.Vec3 Vec3d} parameter.

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface Vec3d + { + /** + *

Whether or not should the {@link Vector3Argument} locator accept an optional direction aside from the + * {@link net.minecraft.world.phys.Vec3}

This parameter can only be used in a {@link Vector3Argument} type, since else there is no way + * to get the direction too.

+ */ + boolean optionalDirection() default false; + + /** + *

Whether or not should the {@link Vector3Argument} locator accept an entity aside to get the {@link Vec3d} from and return that entity + * too

Note that you will only be able to get that entity if the annotation is present in a {@link Vector3Argument}

+ */ + boolean optionalEntity() default false; + } + + /** + *

Represents that the annotated argument must be gotten by passing the arguments in this annotation into a {@link FunctionArgument} + * locator

+ * + *

Can be used in both {@link FunctionArgument} and {@link FunctionValue} types, but the last won't have access to arguments provided to the + * function, even though they will still be consumed from the arguments the function was called with.

+ * + *

This will consume any remaining parameters passed to the function, therefore any other parameter after this will throw.

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface Function + { + /** + *

Whether this Locator should allow no function to be passed.

This is not compatible with {@link FunctionValue} type, since a + * converter returning {@code null} will throw as if the passed argument was incorrect. You can still use it when targeting + * {@link FunctionArgument}

+ */ + boolean allowNone() default false; + + /** + *

Whether the locator should check that the number of arguments passed along with the function matches the number of arguments that the + * located function requires. Note that FunctionLocators consume all remaining arguments even if this is set to {@code false}.

+ */ + boolean checkArgs(); + } + + /** + *

Class that holds locators and methods to get them

+ * + *

Not part of the public API, just that interfaces must have all members public

+ */ + final class Locators + { + private Locators() + { + super(); + } + + static ValueConverter fromAnnotatedType(AnnotatedType annoType, Class type) + { + if (annoType.isAnnotationPresent(Block.class)) + { + return new BlockLocator<>(annoType.getAnnotation(Block.class), type); + } + if (annoType.isAnnotationPresent(Function.class)) + { + return new FunctionLocator<>(annoType.getAnnotation(Function.class), type); + } + if (annoType.isAnnotationPresent(Vec3d.class)) + { + return new Vec3dLocator<>(annoType.getAnnotation(Vec3d.class), type); + } + throw new IllegalStateException("Locator#fromAnnotatedType got called with an incompatible AnnotatedType"); + } + + private static class BlockLocator extends AbstractLocator + { + private final java.util.function.Function returnFunction; + private final boolean acceptString; + private final boolean anyString; + private final boolean optional; + + public BlockLocator(Block annotation, Class type) + { + super(); + this.acceptString = annotation.acceptString(); + this.anyString = annotation.anyString(); + this.optional = annotation.optional(); + if (type != BlockArgument.class && (anyString || optional)) + { + throw new IllegalArgumentException("Can only use anyString or optional parameters of Locator.Block if targeting a BlockArgument"); + } + this.returnFunction = getReturnFunction(type); + if (returnFunction == null) + { + throw new IllegalArgumentException("Locator.Block can only be used against BlockArgument, BlockValue, BlockPos or BlockState types!"); + } + } + + @Nullable + @SuppressWarnings("unchecked") + private static java.util.function.Function getReturnFunction(Class type) + { + if (type == BlockArgument.class) + { + return r -> (R) r; + } + if (type == BlockValue.class) + { + return r -> (R) r.block; + } + if (type == BlockPos.class) + { + return r -> (R) r.block.getPos(); + } + if (type == BlockState.class) + { + return r -> (R) r.block.getBlockState(); + } + return null; + } + + @Override + public String getTypeName() + { + return "block"; + } + + @Override + public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + BlockArgument locator = BlockArgument.findIn((CarpetContext) context, valueIterator, 0, acceptString, optional, anyString); + return returnFunction.apply(locator); + } + } + + private static class Vec3dLocator extends AbstractLocator + { + private final boolean optionalDirection; + private final boolean optionalEntity; + private final boolean returnVec3d; + + public Vec3dLocator(Vec3d annotation, Class type) + { + this.optionalDirection = annotation.optionalDirection(); + this.optionalEntity = annotation.optionalEntity(); + this.returnVec3d = type == net.minecraft.world.phys.Vec3.class; // Because of the locator + if (returnVec3d && optionalDirection) + { + throw new IllegalArgumentException("optionalDirection Locator.Vec3d cannot be used for Vec3d type, use Vector3Argument instead"); + } + if (!returnVec3d && type != Vector3Argument.class) + { + throw new IllegalArgumentException("Locator.Vec3d can only be used in Vector3Argument or Vec3d types"); + } + } + + @Override + public String getTypeName() + { + return "position"; + } + + @Override + public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + Vector3Argument locator = Vector3Argument.findIn(valueIterator, 0, optionalDirection, optionalEntity); + @SuppressWarnings("unchecked") R ret = (R) (returnVec3d ? locator.vec : locator); + return ret; + } + } + + private static class FunctionLocator extends AbstractLocator + { + private final boolean returnFunctionValue; + private final boolean allowNone; + private final boolean checkArgs; + + FunctionLocator(Function annotation, Class type) + { + super(); + this.returnFunctionValue = type == FunctionValue.class; + if (!returnFunctionValue && type != FunctionArgument.class) + { + throw new IllegalArgumentException("Params annotated with Locator.Function must be of either FunctionArgument or FunctionValue type"); + } + this.allowNone = annotation.allowNone(); + this.checkArgs = annotation.checkArgs(); + if (returnFunctionValue && allowNone) + { + throw new IllegalArgumentException("Cannot use allowNone of Locator.Function in FunctionValue types, use FunctionArgument"); + } + } + + @Override + public R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + Module module = context.host.main; + FunctionArgument locator = FunctionArgument.findIn(context, module, Lists.newArrayList(valueIterator), 0, allowNone, checkArgs); + @SuppressWarnings("unchecked") R ret = (R) (returnFunctionValue ? locator.function : locator); + return ret; + } + + @Override + public String getTypeName() + { + return "function"; + } + } + + private abstract static class AbstractLocator implements ValueConverter, Locator + { + @Override + public R convert(Value value, @Nullable Context context) + { + throw new UnsupportedOperationException("Cannot call a locator in a parameter that doesn't contain a context!"); + } + + @Override + public boolean consumesVariableArgs() + { + return true; + } + + @Override + public int valueConsumption() + { + return 1; + } + + @Override + public abstract R checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT); + } + + } +} diff --git a/src/main/java/carpet/script/annotation/MapConverter.java b/src/main/java/carpet/script/annotation/MapConverter.java new file mode 100644 index 0000000..dc818a2 --- /dev/null +++ b/src/main/java/carpet/script/annotation/MapConverter.java @@ -0,0 +1,183 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import carpet.script.Context; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +/** + *

Converts a {@link MapValue} to a {@link Map}, converting all of its contents to their respective types.

+ * + *

If the {@link Param.KeyValuePairs} annotation is specified, uses its subclass at {@link PairConverter} and allows passing either a map, a list + * like [key, value, key2, value2,...] or the same as the list inlined in the function's body, unless {@link Param.KeyValuePairs#allowMultiparam()} is + * set to {@code false}.

+ * + *

Maps provided by this converter are not linked to the initial map, and therefore will not reflect changes in either of them.

+ * + * @param The type of the map's keys + * @param The type of the map's values + */ +class MapConverter implements ValueConverter> +{ + protected final ValueConverter keyConverter; + protected final ValueConverter valueConverter; + + @Override + public String getTypeName() + { + return "map with " + keyConverter.getTypeName() + "s as the key and " + valueConverter.getTypeName() + "s as the value"; + } + + @Override + public Map convert(Value value, @Nullable Context context) + { + Map result = new HashMap<>(); + if (value instanceof MapValue) + { + for (Entry entry : ((MapValue) value).getMap().entrySet()) + { + K key = keyConverter.convert(entry.getKey(), context); + V val = valueConverter.convert(entry.getValue(), context); + if (key == null || val == null) + { + return null; + } + result.put(key, val); + } + return result; + } + return null; + } + + private MapConverter(AnnotatedType keyType, AnnotatedType valueType) + { + super(); + keyConverter = ValueConverter.fromAnnotatedType(keyType); + valueConverter = ValueConverter.fromAnnotatedType(valueType); + } + + /** + *

Returns a new {@link MapConverter} to convert to the given {@link AnnotatedType}.

+ * + *

The returned {@link ValueConverter} will convert the objects inside the map (keys and values) to the generics specified in the + * {@link AnnotatedType}.

+ * + *

If the provided {@link AnnotatedType} is annotated with {@link Param.KeyValuePairs}, it will return an implementation of + * {@link MapConverter} that provides extra checks in order to allow inputs of either {@code {k->v,k2->v2,...}}, {@code [k,v,k2,v2,...]} or + * {@code fn(...,k,v,k2,v2,...)}

+ * + * @apiNote This method expects the {@link AnnotatedType} to already be of {@link Map} type, and, while it will technically accept a + * non-{@link Map} {@link AnnotatedType}, it will fail if it doesn't has at least two generic parameters with an + * {@link ArrayIndexOutOfBoundsException}. + * @param annotatedType The type to get generics information from + * @return A new {@link MapConverter} for the data specified in the {@link AnnotatedType} + */ + static MapConverter fromAnnotatedType(AnnotatedType annotatedType) + { + AnnotatedType[] annotatedGenerics = ((AnnotatedParameterizedType) annotatedType).getAnnotatedActualTypeArguments(); + return annotatedType.isAnnotationPresent(Param.KeyValuePairs.class) + ? new PairConverter<>(annotatedGenerics[0], annotatedGenerics[1], annotatedType.getAnnotation(Param.KeyValuePairs.class)) + : new MapConverter<>(annotatedGenerics[0], annotatedGenerics[1]); + } + + private static final class PairConverter extends MapConverter + { + private final boolean acceptMultiParam; + + private PairConverter(AnnotatedType keyType, AnnotatedType valueType, Param.KeyValuePairs config) + { + super(keyType, valueType); + acceptMultiParam = config.allowMultiparam(); + } + + @Override + public boolean consumesVariableArgs() + { + return acceptMultiParam; + } + + @Nullable + @Override + public Map convert(Value value, @Nullable Context context) { + return value instanceof MapValue ? super.convert(value, context) + : value instanceof ListValue ? convertList(((ListValue)value).getItems(), context) + : null; // Multiparam mode can only be used in evalAndConvert + } + + + @Nullable + private Map convertList(List valueList, @Nullable Context context) + { + if (valueList.size() % 2 == 1) + { + return null; + } + Map map = new HashMap<>(); + Iterator val = valueList.iterator(); + while (val.hasNext()) + { + K key = keyConverter.convert(val.next(), context); + V value = valueConverter.convert(val.next(), context); + if (key == null || value == null) + { + return null; + } + map.put(key, value); + } + return map; + } + + @Nullable + @Override + public Map checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + if (!valueIterator.hasNext()) + { + return null; + } + Value val = valueIterator.next(); + if (!acceptMultiParam || val instanceof MapValue || (val instanceof ListValue && !(keyConverter instanceof ListConverter))) + { + return convert(val, context); // @KeyValuePairs Map, Boolean> will not support list consumption + } + + Map map = new HashMap<>(); + K key = keyConverter.convert(val, context); //First pair is manual since we got it to check for a different conversion mode + V value = valueConverter.checkAndConvert(valueIterator, context, theLazyT); + if (key == null || value == null) + { + return null; + } + map.put(key, value); + while (valueIterator.hasNext()) + { + key = keyConverter.checkAndConvert(valueIterator, context, theLazyT); + value = valueConverter.checkAndConvert(valueIterator, context, theLazyT); + if (key == null || value == null) + { + return null; + } + map.put(key, value); + } + return map; + } + + @Override + public String getTypeName() + { + return "either a map of key-value pairs" + (acceptMultiParam ? "," : " or") + " a list in the form of [key, value, key2, value2,...]" + + (acceptMultiParam ? " or those key-value pairs in the function" : "") + " (keys being " + keyConverter.getTypeName() + + "s and values being " + valueConverter.getTypeName() + "s)"; + } + } +} diff --git a/src/main/java/carpet/script/annotation/OptionalConverter.java b/src/main/java/carpet/script/annotation/OptionalConverter.java new file mode 100644 index 0000000..462bf83 --- /dev/null +++ b/src/main/java/carpet/script/annotation/OptionalConverter.java @@ -0,0 +1,119 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Optional; + +import carpet.script.Context; +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +/** + *

{@link ValueConverter} that accepts a parameter to not be present on function call.

+ * + *

Note that it will not work properly if it's not at the end of the function, since values are consumed in an ordered way, therefore it will throw + * if the value is not present since it will try to evaluate the next parameter into {@code } and horribly fail, or it will draw values from the + * {@link ValueConverter}s after it, horribly failing too..

+ * + *

Why {@link Optional}?

+ * + *

It allows passing three states:

+ *
    + *
  • A wrapped object, if the value was present and correct
  • + *
  • An incorrect value ({@code null}), if the value was present but incorrect, as per the contract of {@link ValueConverter} and the way + * {@link AnnotationParser} will consider {@link null} values
  • + *
  • An empty {@link Optional}, if the value was not present (or {@code null})
  • + *
+ * + * @param The type of the internal {@link ValueConverter}, basically the generic type of the {@link Optional} + */ +final class OptionalConverter implements ValueConverter> +{ + private final ValueConverter typeConverter; + + @Override + public String getTypeName() + { + return "optional " + typeConverter.getTypeName(); + } + + private OptionalConverter(AnnotatedType type) + { + typeConverter = ValueConverter.fromAnnotatedType(type); + } + + /** + * {@inheritDoc} + * + * @implNote Unlike most other converters, {@link OptionalConverter} will not call this method from + * {@link #checkAndConvert(Iterator, Context, Context.Type)} and is only used as a fallback in types that don't support it. + */ + @Nullable + @Override + public Optional convert(Value value, @Nullable Context context) + { + if (value.isNull()) + { + return Optional.empty(); + } + R converted = typeConverter.convert(value, context); + if (converted == null) + { + return null; + } + return Optional.of(converted); + } + + @Nullable + @Override + public Optional checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + if (!valueIterator.hasNext() || valueIterator.next().isNull()) + { + return Optional.empty(); + } + ((ListIterator) valueIterator).previous(); + R converted = typeConverter.checkAndConvert(valueIterator, context, theLazyT); + if (converted == null) + { + return null; + } + return Optional.of(converted); + } + + @Override + public boolean consumesVariableArgs() + { + return true; + } + + @Override + public int valueConsumption() + { + return 0; // Optional parameters therefore require a minimum of 0 + } + + /** + *

Returns a new {@link OptionalConverter} to convert to the type in the given {@link AnnotatedType} if there is at least one element left in + * the iterator, with any parameters, annotations or anything that was present in there.

+ * + *

The given {@link ValueConverter} will be called after ensuring that there is at least one element left in the iterator to the type specified + * in the generics of this {@link AnnotatedType}, or return an empty {@link Optional} if there is nothing left or the first value meets + * {@link Value#isNull()}. + * + * @apiNote This method expects the {@link AnnotatedType} to already be of {@link Optional} type, and, while it will technically accept a + * non-{@link Optional} {@link AnnotatedType}, it will fail with an {@link ArrayIndexOutOfBoundsException} if it doesn't has at least one + * generic parameter. + * @param annotatedType The type to get generics information from + * @return A new {@link OptionalConverter} for the data specified in the {@link AnnotatedType} + */ + static OptionalConverter fromAnnotatedType(AnnotatedType annotatedType) + { + AnnotatedParameterizedType paramType = (AnnotatedParameterizedType) annotatedType; + AnnotatedType wrappedType = paramType.getAnnotatedActualTypeArguments()[0]; + return new OptionalConverter<>(wrappedType); + } +} diff --git a/src/main/java/carpet/script/annotation/OutputConverter.java b/src/main/java/carpet/script/annotation/OutputConverter.java new file mode 100644 index 0000000..1562957 --- /dev/null +++ b/src/main/java/carpet/script/annotation/OutputConverter.java @@ -0,0 +1,138 @@ +package carpet.script.annotation; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; +import org.apache.commons.lang3.ClassUtils; + +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import javax.annotation.Nullable; + +/** + *

A converter from a given {@link Object} of type T into a {@link Value}, used in order to convert the outputs of methods into usable Scarpet + * values.

+ * + * @see #register(Class, BiFunction) + * @param The type to convert from into a {@link Value} + */ +public final class OutputConverter +{ + private static final Map, OutputConverter> byResult = new HashMap<>(); + private static final OutputConverter VALUE = new OutputConverter<>((v, regs) -> v); + static + { + // Primitives are handled. Things are boxed in the process anyway, therefore would recommend boxed outputs, so you can use null + register(Void.TYPE, v -> Value.NULL); + register(Boolean.class, BooleanValue::of); + register(Integer.class, (v, r) -> new NumericValue(v)); + register(Double.class, NumericValue::of); + register(Float.class, NumericValue::of); + register(Long.class, (v, r) -> new NumericValue(v)); + register(String.class, StringValue::new); + register(Entity.class, EntityValue::new); + register(Component.class, FormattedTextValue::new); + register(Tag.class, NBTSerializableValue::new); + register(BlockPos.class, (v, r) -> ValueConversions.of(v)); + register(Vec3.class, (v, r) -> ValueConversions.of(v)); + register(ItemStack.class, (v, r) -> ValueConversions.of(v, r)); + register(ResourceLocation.class, (v, r) -> ValueConversions.of(v)); + register(GlobalPos.class, (v, r) -> ValueConversions.of(v)); + } + + private final BiFunction converter; + + private OutputConverter(BiFunction converter) + { + this.converter = converter; + } + + /** + *

Returns the {@link OutputConverter} for the specified returnType.

+ * + * @param The type of the {@link OutputConverter} you are looking for + * @param returnType The class that the returned {@link OutputConverter} converts from + * @return The {@link OutputConverter} for the specified returnType + */ + @SuppressWarnings("unchecked") // OutputConverters are stored with their class, for sure since the map is private (&& class has same generic as + // converter) + public static OutputConverter get(Class returnType) + { + if (Value.class.isAssignableFrom(returnType)) + { + return (OutputConverter) VALUE; + } + returnType = (Class) ClassUtils.primitiveToWrapper(returnType); // wrapper holds same generic as primitive: wrapped + return (OutputConverter) Objects.requireNonNull(byResult.get(returnType), + "Unregistered output type: " + returnType + ". Register in OutputConverter"); + } + + /** + *

Converts the given input object into a {@link Value}, to be used in return values of Scarpet functions

+ * + *

Returns {@link Value#NULL} if passed a {@code null} input

+ * + * @param input The value to convert + * @return The converted value + */ + public Value convert(@Nullable T input, RegistryAccess regs) + { + return input == null ? Value.NULL : converter.apply(input, regs); + } + + /** + *

Registers a new type to be able to be used as the return value of methods, converting from inputType to a {@link Value} + * using the given function.

+ * + * @param The type of the input type + * @param inputType The class of T + * @param converter The function that converts the instance of T to a {@link Value} + */ + public static void register(Class inputType, BiFunction converter) + { + OutputConverter instance = new OutputConverter<>(converter); + if (byResult.containsKey(inputType)) + { + throw new IllegalArgumentException(inputType + " already has a registered OutputConverter"); + } + byResult.put(inputType, instance); + } + public static void register(Class inputType, Function converter) + { + register(inputType, (v, regs) -> converter.apply(v)); + } + + /** + * @see #register(Class, BiFunction) + * + * @deprecated Just use {@link #register(Class, BiFunction)}, it now does the same as this + */ + @Deprecated + public static void registerToValue(Class inputType, BiFunction converter) + { + register(inputType, converter); + } + @Deprecated + public static void registerToValue(Class inputType, Function converter) + { + register(inputType, (v, regs) -> converter.apply(v)); + } +} diff --git a/src/main/java/carpet/script/annotation/Param.java b/src/main/java/carpet/script/annotation/Param.java new file mode 100644 index 0000000..665d602 --- /dev/null +++ b/src/main/java/carpet/script/annotation/Param.java @@ -0,0 +1,304 @@ +package carpet.script.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedType; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +import carpet.script.Context; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + *

Class that holds annotations for Scarpet parameters.

+ * + * @see Param.Strict + * @see Param.AllowSingleton + * @see Param.Custom + * @see Locator.Block + * @see Locator.Vec3d + * @see Locator.Function + * + */ +public interface Param +{ + /** + *

Determines that this parameter accepts being passing a value directly instead of a list of those values.

+ * + *

Can only be used in {@link List} parameters.

+ * + *

The function method will receive a singleton of the item in question if there's a single value.

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface AllowSingleton + { + + } + + /** + *

Determines that this (and optionally the following parameters) accept either a map of the specified key-value pairs, a list type [key, + * value, key2, value2,...] or the same as the list but directly in the parameters (can be disabled in {@link #allowMultiparam()}).

+ * + *

Can only be used in {@link Map} types, and {@link #allowMultiparam()} requires it to not be in a type parameter (since lists and maps + * contains groups of single items)

+ * + *

IMPORTANT: Using this annotation with {@link #allowMultiparam()} will make this element consume each and every remaining value in the + * function call, therefore it will cause any other parameters (that are not varargs) to throw as if they were not present, unless they are + * optional (defined by using Java's {@link Optional} type). They could only be accessed if the parameter at this location is specifically a list + * or map.
Having it as {@code true} will also cause the function to be considered of variable arguments even if it doesn't have varargs.

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface KeyValuePairs + { + /** + *

Whether or not this accepts the key-value pairs directly in the function call as myFunction(..., key, value, key2, value2)

+ * + *

Having this set to {@code true} (as it is by default) has the side effects of effectively converting the method in a variable parameter + * count method, and consuming everything remaining in the parameter list unless it finds as first parameter a map or list to generate the map + * from, causing any following parameters (except varargs) to throw as if they were not present, unless they are optional.

+ */ + boolean allowMultiparam() default true; + } + + /** + *

Defines that the parameter's converter has to be retrieved from the custom converter storage, in order to allow extensions to register + * complex {@link ValueConverter}s. You can register such converters in {@link Params#registerCustomConverterFactory(BiFunction)}, although + * if you only need a simple {@link Value}->something converter you should be looking at {@link SimpleTypeConverter} instead

+ */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface Custom + { + + } + + /** + *

Defines that a parameter of type {@link String}, {@link Component}, {@link ServerPlayer}, {@link Boolean} or other registered strict type + * must be of its corresponding {@link Value} in order to be accepted (respectively {@link StringValue}, {@link FormattedTextValue}, + * {@link EntityValue} or {@link BooleanValue}).

+ * + *

If this annotation is not specified, Carpet will accept any other {@link Value} and call respectively {@link Value#getString()}, + * {@code new LiteralText(Value#getString())}, {@link EntityValue#getPlayerByValue(MinecraftServer, Value)} or {@link Value#getBoolean()}.

+ * + *

You can define "shallow strictness" ({@link #shallow()}) if you want to allow passing both a {@link StringValue} or a + * {@link FormattedTextValue} to a {@link Component} parameter or a {@link NumericValue} to a {@link BooleanValue}, but not any {@link Value}.

+ * + */ + @Documented + @Retention(RUNTIME) + @Target({ PARAMETER, TYPE_USE }) + @interface Strict + { + /** + *

Defines whether this parameter can accept types with "shallow strictness", that is, in order to get a {@link Component}, accepting either a + * {@link StringValue} or a {@link FormattedTextValue} as the parameter, or in order to get a {@link Boolean}, accepting either a + * {@link NumericValue} or a {@link BooleanValue}.

+ * + *

Without shallow mode, it would only accept from specifically a {@link FormattedTextValue} or {@link BooleanValue} respectively. + * + *

Using this in an unsupported type will throw {@link IllegalArgumentException}, just as if you used the annotation in an unsupported + * type.

+ * + *

This is {@code false} by default.

+ */ + boolean shallow() default false; + } + + /** + *

Class that holds the actual converters and converter getting logic for those annotated types and things.

+ * + *

It also holds the registry for strict and custom {@link ValueConverter}s.

+ * + * @see #registerStrictConverter(Class, boolean, ValueConverter) + * @see #registerCustomConverterFactory(BiFunction) + * + */ + final class Params + { + /** + *

A {@link ValueConverter} that outputs the {@link Context} in which the function has been called, and throws {@link UnsupportedOperationException} when trying to convert a {@link Value} + * directly.

+ */ + static final ValueConverter CONTEXT_PROVIDER = new ValueConverter<>() + { + @Nullable + @Override + public String getTypeName() + { + return null; + } + + @Override + public Context convert(Value value, @Nullable Context context) + { + throw new UnsupportedOperationException("Called convert() with Value in Context Provider converter, where only checkAndConvert is supported"); + } + + @Override + public Context checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + return context; + } + + @Override + public int valueConsumption() + { + return 0; + } + }; + + /** + *

A {@link ValueConverter} that outputs the {@link Context.Type} which the function has been called, or throws {@link UnsupportedOperationException} when trying to convert a {@link Value} + * directly.

+ */ + static final ValueConverter CONTEXT_TYPE_PROVIDER = new ValueConverter<>() + { + @Nullable + @Override + public String getTypeName() + { + return null; + } + + @Override + public Context.Type convert(Value value, @Nullable Context context) + { + throw new UnsupportedOperationException("Called convert() with a Value in TheLazyT Provider, where only checkAndConvert is supported"); + } + + @Override + public Context.Type checkAndConvert(Iterator valueIterator, Context context, Context.Type theLazyT) + { + return theLazyT; + } + + @Override + public int valueConsumption() + { + return 0; + } + }; + + record StrictConverterInfo(Class type, boolean shallow) {} + private static final Map> strictParamsByClassAndShallowness = new HashMap<>(); + static + { // TODO Specify strictness in name? + registerStrictConverter(String.class, false, new SimpleTypeConverter<>(StringValue.class, StringValue::getString, "string")); + registerStrictConverter(Component.class, false, new SimpleTypeConverter<>(FormattedTextValue.class, FormattedTextValue::getText, "text")); + registerStrictConverter(Component.class, true, new SimpleTypeConverter<>(StringValue.class, FormattedTextValue::getTextByValue, "text")); + registerStrictConverter(ServerPlayer.class, false, new SimpleTypeConverter<>(EntityValue.class, + v -> EntityValue.getPlayerByValue(v.getEntity().getServer(), v), "online player entity")); + registerStrictConverter(Boolean.class, false, new SimpleTypeConverter<>(BooleanValue.class, BooleanValue::getBoolean, "boolean")); + registerStrictConverter(Boolean.class, true, new SimpleTypeConverter<>(NumericValue.class, NumericValue::getBoolean, "boolean")); + } + + /** + * Ya' know, gets the {@link ValueConverter} given the {@link Strict} annotation. + * + * @param type The {@link AnnotatedType} to search the annotation data and class in + * @return The {@link ValueConverter} for the specified type and annotation data + * @throws IllegalArgumentException If the type doesn't accept the {@link Strict} annotation or if it has been used incorrectly (shallow in + * unsupported places) + */ + static ValueConverter getStrictConverter(AnnotatedType type) + { + boolean shallow = type.getAnnotation(Strict.class).shallow(); + Class clazz = (Class) type.getType(); + StrictConverterInfo key = new StrictConverterInfo(clazz, shallow); + ValueConverter converter = strictParamsByClassAndShallowness.get(key); + if (converter != null) + { + return converter; + } + throw new IllegalArgumentException("Incorrect use of @Param.Strict annotation"); + } + + /** + *

Registers a new {@link Param.Strict} parameter converter with the specified shallowness.

+ * + *

Registered types should follow the general contract of the rest of {@link Param.Strict} parameter converters, that is, don't have a + * shallow-strict converter registered without having a fully-strict converter available. In order to register completely non-strict + * converters, those should be registered in their respective {@link ValueConverter} classes, usually in {@link SimpleTypeConverter}.

+ * + * @param The type class of the return type of the given converter and therefore the generic of itself + * @param type The class instance of the conversion result. + * @param shallow {@code true} if you are registering a shallow-strict parameter, {@code false} if a "fully" strict one + * @param converter The {@link ValueConverter} for the given type and shallowness. + */ + public static void registerStrictConverter(Class type, boolean shallow, ValueConverter converter) + { + StrictConverterInfo key = new StrictConverterInfo(type, shallow); + if (strictParamsByClassAndShallowness.containsKey(key)) + { + throw new IllegalArgumentException(type + " already has a registered " + (shallow ? "" : "non-") + "shallow StrictConverter"); + } + strictParamsByClassAndShallowness.put(key, converter); + } + + private static final List, ValueConverter>> customFactories = new ArrayList<>(); + + /** + *

Allows extensions to register COMPLEX {@link ValueConverter} factories in order to be used with the {@link Param.Custom} + * annotation.

If you only need to register a converter from a {@link Value} to a type, use + * {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)} instead. This is intended to be used when you + * need more granular control over the conversion, such as custom extra parameters via annotations, converters using multiple values, or even + * a variable number of values.

The annotation parser will loop through all registered custom converter factories when searching + * for the appropriate {@link ValueConverter} for a parameter annotated with the {@link Param.Custom} annotation.

Factories are + * expected to return {@code null} when the provided arguments don't match a {@link ValueConverter} they are able to create (or reuse).

+ *

You have {@link ValueCaster#get(Class)} and {@link ValueConverter#fromAnnotatedType(AnnotatedType)} available in case you need to get + * valid {@link ValueConverter}s for things such as nested types, intermediary conversions or whatever you really need them for.

+ * + * @param The type that the ValueConverter will convert to. Its class will also be passed to the factory + * @param factory A {@link BiFunction} that provides {@link ValueConverter}s given an {@link AnnotatedType} and the {@link Class} of its type, + * for convenience reasons. The factory must return {@code null} if the specific conditions required to return a valid + * converter are not met, therefore letting other registered factories try get theirs. Factories must also ensure that the + * returned {@link ValueConverter} converts to the given {@link Class}, and that the {@link ValueConverter} follows the + * contract of {@link ValueConverter}s, which can be found in its Javadoc. Factories should try to be specific in order to + * avoid possible collisions with other extensions. + */ + @SuppressWarnings("unchecked") // this makes no sense... But I guess its preferable to enforce typesafety in callers + public static void registerCustomConverterFactory(BiFunction, ValueConverter> factory) + { + customFactories.add((BiFunction, ValueConverter>) (Object) factory); + } + + @SuppressWarnings("unchecked") // Stored correctly + static ValueConverter getCustomConverter(AnnotatedType annoType, Class type) + { + ValueConverter result; + for (BiFunction, ValueConverter> factory : customFactories) + { + if ((result = (ValueConverter) factory.apply(annoType, type)) != null) + { + return result; + } + } + throw new IllegalArgumentException("No custom converter found for Param.Custom annotated param with type " + annoType.getType().getTypeName()); + } + } +} diff --git a/src/main/java/carpet/script/annotation/ScarpetFunction.java b/src/main/java/carpet/script/annotation/ScarpetFunction.java new file mode 100644 index 0000000..aada5f0 --- /dev/null +++ b/src/main/java/carpet/script/annotation/ScarpetFunction.java @@ -0,0 +1,98 @@ +package carpet.script.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Optional; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import carpet.script.Context; +import carpet.script.LazyValue; +import carpet.script.value.Value; + +/** + *

Defines a method that can be used as a function in the Scarpet language.

+ * + *

Methods annotated with this annotation are not required to accept and return the implementation {@code Context context, Context.Type t, List lv}, + * but instead can specify whatever parameters they actually need that will be automatically converted from their respective {@link Value}s and passed to + * the method as the expected type. Functions will automatically fail if given parameters are not compatible with the specified ones, or if the number + * of provided arguments is either too large or too small.

+ * + *

Types to be used in those functions must be already registered in their respective {@link ValueConverter} implementations.
+ * In order to register a new type to convert to, you can do so in {@link SimpleTypeConverter#registerType(Class, Class, java.util.function.Function, String)}, + * and in order to register a new variant of {@link Value}, use {@link ValueCaster#register(Class, String)}.
+ * In order to convert the output of your method to a {@link LazyValue} you will also need to register its conversion in {@link OutputConverter}

+ * + *

In order for Carpet to find methods annotated with this annotation, you must add your function class(es) to Carpet by running + * {@link AnnotationParser#parseFunctionClass(Class)} ONCE.

+ * + *

Methods annotated with this annotation must not declare throwing any checked exceptions.

+ * + *

If one of the method's parameters is {@link Context}, Carpet will pass the actual {@link Context} of the expression to the + * method. If one of the method's parameters is {@link Context.Type}, Carpet will pass the Context Type the function was called inside. That + * is different from the Context Type provided in this annotation in that it's the one it was called with, while the one in the annotation + * is the one that will be used when evaluating the lazy values passed to the function.

+ * + * @see AnnotationParser + * @see AnnotationParser#parseFunctionClass(Class) + * @see Param.Strict + * @see Param.AllowSingleton + * @see Param.KeyValuePairs + * @see Param.Custom + * @see Locator.Block + * @see Locator.Vec3d + * @see Optional + */ +@Documented +@Target(METHOD) +@Retention(RUNTIME) +public @interface ScarpetFunction +{ + /** + *

Used to define that this {@link ScarpetFunction} can accept an unlimited number of parameters

+ */ + int UNLIMITED_PARAMS = -1; + + /** + *

If the function can accept a variable number of parameters, either by declaring its last parameter as a varargs parameter or by having one + * of their parameters use a converter that consumes a variable number of arguments, this must define the maximum number of parameters this + * function can take.

+ * + *

The parser will throw in case a function can accept a variable number of parameters but no maxParams value has been specified in its + * {@link ScarpetFunction} annotation.
+ * The value, however, will be ignored if the function has a fixed number of parameters.

+ * + *

Note that this maximum number of parameters refers to the limit of parameters that can be passed to a function from Scarpet, not the maximum + * number of parameters the method will receive in its varargs parameter. Therefore, if using, for example, a locator argument, you should + * consider that those can take either a single triple of values or 3 independent values, that would be counted in the maximum number of + * parameters.

+ * + *

Use {@link ScarpetFunction#UNLIMITED_PARAMS} to allow an unlimited number of parameters.

+ * + * @return The maximum number of parameters this function can accept + */ + int maxParams() default AnnotationParser.UNDEFINED_PARAMS; + + /** + *

The name of the function in Scarpet, that by default will be the method name.

+ * + *

The convention in Scarpet is to use names in snake case.

+ * + * @return The name for this function in Scarpet + */ + String functionName() default AnnotationParser.USE_METHOD_NAME; + + /** + *

Defines the Context Type that will be used when evaluating arguments to annotated methods.

+ * + *

Note that this is not the same as the output from a {@link Context.Type} parameter, since that returns the Context Type the method was + * called with, while this defines what Context Type will be used to evaluate the arguments.

+ * + *

Defaults to {@link Context.Type#NONE}, like any regular ContextFunctions

+ * + * @see Context + */ + Context.Type contextType() default Context.Type.NONE; +} diff --git a/src/main/java/carpet/script/annotation/SimpleTypeConverter.java b/src/main/java/carpet/script/annotation/SimpleTypeConverter.java new file mode 100644 index 0000000..87c4f25 --- /dev/null +++ b/src/main/java/carpet/script/annotation/SimpleTypeConverter.java @@ -0,0 +1,155 @@ +package carpet.script.annotation; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import carpet.script.CarpetContext; +import carpet.script.Context; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import javax.annotation.Nullable; + +/** + *

A simple {@link ValueConverter} implementation that converts from a specified subclass of {@link Value} into {@code } by using a given + * function.

+ * + *

{@link SimpleTypeConverter}s are reused whenever asked for one, since they don't have any complexity.

+ * + * @see #registerType(Class, Class, Function, String) + * @see #registerType(Class, Class, BiFunction, String) + * + * @param The type of the required input {@link Value} + * @param The type that this converter converts to + */ +public final class SimpleTypeConverter implements ValueConverter +{ + private static final Map, SimpleTypeConverter> byResult = new HashMap<>(); + static + { + registerType(Value.class, ServerPlayer.class, (v, c) -> EntityValue.getPlayerByValue(((CarpetContext)c).server(), v), "online player"); + registerType(EntityValue.class, Entity.class, EntityValue::getEntity, "entity"); + registerType(Value.class, Level.class, (v, c) -> ValueConversions.dimFromValue(v, ((CarpetContext)c).server()), "dimension"); + registerType(Value.class, Component.class, FormattedTextValue::getTextByValue, "text"); + registerType(Value.class, String.class, Value::getString, "string"); // Check out @Param.Strict for more specific types + + // Primitives are also query those classes + registerType(NumericValue.class, Long.class, NumericValue::getLong, "number"); + registerType(NumericValue.class, Double.class, NumericValue::getDouble, "number"); + registerType(NumericValue.class, Integer.class, NumericValue::getInt, "number"); + registerType(Value.class, Boolean.class, Value::getBoolean, "boolean"); // Check out @Param.Strict for more specific types + } + + private final BiFunction converter; + private final Class valueClass; + private final String typeName; + + /** + * Same as {@link #SimpleTypeConverter(Class, BiFunction, String)}, but without the converter function taking a {@link Context} + * + * @param inputType The required type for the input {@link Value} + * @param converter The function to convert an instance of inputType into R, with the context of the call. + * @param typeName The name of the type for error messages + * + * @see #SimpleTypeConverter(Class, BiFunction, String) + */ + public SimpleTypeConverter(Class inputType, Function converter, String typeName) + { + this(inputType, (v, c) -> converter.apply(v), typeName); + } + + /** + *

A constructor for {@link SimpleTypeConverter}.

+ * + *

This is public in order to provide an implementation to use when registering {@link ValueConverter}s for the {@link Param.Strict} annotation + * registry, and it's not intended way to register new {@link SimpleTypeConverter}

Use {@link #registerType(Class, Class, Function, String)} for + * that.

+ * + * @param inputType The required type for the input {@link Value} + * @param converter The function to convert an instance of inputType into R, with the context of the call. + * @param typeName The name of the type for error messages + */ + public SimpleTypeConverter(Class inputType, BiFunction converter, String typeName) + { + super(); + this.converter = converter; + this.valueClass = inputType; + this.typeName = typeName; + } + + @Override + public String getTypeName() + { + return typeName; + } + + /** + * Returns the {@link SimpleTypeConverter} for the specified outputType. + * + * @param The type of the {@link SimpleTypeConverter} you are looking for + * @param outputType The class that the returned {@link SimpleTypeConverter} converts to + * @return The {@link SimpleTypeConverter} for the specified outputType + */ + @SuppressWarnings("unchecked") // T always extends Value, R is always the same as map's key, since map is private. + static SimpleTypeConverter get(Class outputType) + { + return (SimpleTypeConverter) byResult.get(outputType); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") // more than checked. not using class.cast because then "method is too big" for inlining, because javac is useless + public R convert(Value value, @Nullable Context context) // and adds millions of casts. This one is even removed + { + return valueClass.isInstance(value) ? converter.apply((T)value, context) : null; + } + + /** + *

Registers a new conversion from a {@link Value} subclass to a Java type.

+ * + * @param The {@link Value} subtype required for this conversion, for automatic checked casting + * @param The type of the resulting object + * @param requiredInputType The {@link Class} of {@code } + * @param outputType The {@link Class} of {@code >} + * @param converter A bi function that converts from the given {@link Value} subtype to the given type. Should ideally return {@code null} + * when given {@link Value} cannot be converted to the {@code }, to follow the {@link ValueConverter} contract, but it + * can also throw an {@link InternalExpressionException} by itself if really necessary. + * @param typeName The name of the type, following the conventions of {@link ValueConverter#getTypeName()} + * + * @see #registerType(Class, Class, BiFunction, String) + */ + public static void registerType(Class requiredInputType, Class outputType, + Function converter, String typeName) + { + registerType(requiredInputType, outputType, (val, ctx) -> converter.apply(val), typeName); + } + + /** + *

Registers a new conversion from a {@link Value} subclass to a Java type, with the context of the call.

+ * + * @param The {@link Value} subtype required for this conversion, for automatic checked casting + * @param The type of the resulting object + * @param requiredInputType The {@link Class} of {@code } + * @param outputType The {@link Class} of {@code >} + * @param converter A function that converts from the given {@link Value} subtype to the given type. Should ideally return {@code null} + * when given {@link Value} cannot be converted to the {@code }, to follow the {@link ValueConverter} contract, but it + * can also throw an {@link InternalExpressionException} by itself if really necessary. + * @param typeName The name of the type, following the conventions of {@link ValueConverter#getTypeName()} + */ + public static void registerType(Class requiredInputType, Class outputType, + BiFunction converter, String typeName) + { + SimpleTypeConverter type = new SimpleTypeConverter<>(requiredInputType, converter, typeName); + byResult.put(outputType, type); + } +} diff --git a/src/main/java/carpet/script/annotation/ValueCaster.java b/src/main/java/carpet/script/annotation/ValueCaster.java new file mode 100644 index 0000000..6cd2624 --- /dev/null +++ b/src/main/java/carpet/script/annotation/ValueCaster.java @@ -0,0 +1,109 @@ +package carpet.script.annotation; + +import java.util.HashMap; +import java.util.Map; + +import carpet.script.Context; +import carpet.script.value.AbstractListValue; +import carpet.script.value.BlockValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.ThreadValue; +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +/** + *

Simple {@link ValueConverter} implementation that casts a {@link Value} into one of its subclasses, either for use directly in parameters or + * converters, or as an already working middle step.

+ * + *

{@link ValueCaster}s are reused whenever asked for one, since they don't have any complexity.

+ * + * @see #register(Class, String) + * + * @param The {@link Value} subclass this {@link ValueCaster} casts to + */ +public final class ValueCaster implements ValueConverter // R always extends Value, not explicitly because of type checking +{ + private static final Map, ValueCaster> byResult = new HashMap<>(); + static + { + register(Value.class, "value"); + register(BlockValue.class, "block"); + register(EntityValue.class, "entity"); + register(FormattedTextValue.class, "formatted text"); + register(FunctionValue.class, "function"); + register(ListValue.class, "list"); + register(MapValue.class, "map"); + register(AbstractListValue.class, "list or similar"); // For LazyListValue basically? Not sure what should use this + register(NBTSerializableValue.class, "nbt object"); + register(NumericValue.class, "number"); + register(BooleanValue.class, "boolean"); + register(StringValue.class, "string"); + register(ThreadValue.class, "thread"); + } + + private final Class outputType; + private final String typeName; + + private ValueCaster(Class outputType, String typeName) + { + super(); + this.outputType = outputType; + this.typeName = typeName; + } + + @Override + public String getTypeName() + { + return typeName; + } + + /** + *

Returns the registered {@link ValueCaster} for the specified outputType.

+ * + * @param The type of the {@link ValueCaster} you are looking for + * @param outputType The class of the {@link Value} the returned {@link ValueCaster} casts to + * @return The {@link ValueCaster} for the specified outputType + */ + @SuppressWarnings("unchecked") // Casters are stored with their exact class, for sure since the map is private (&& class has same generic as + // caster) + public static ValueCaster get(Class outputType) + { + return (ValueCaster) byResult.get(outputType); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") // more than checked, see SimpleTypeConverter#converter for reasoning + public R convert(Value value, @Nullable Context context) + { + if (!outputType.isInstance(value)) + { + return null; + } + return (R)value; + } + + /** + *

Registers a new {@link Value} to be able to use it in {@link SimpleTypeConverter}

+ * + * @param The {@link Value} subclass + * @param valueClass The class of T + * @param typeName A {@link String} representing the name of this type. It will be used in error messages when there is no higher type + * required + */ + public static void register(Class valueClass, String typeName) + { + ValueCaster caster = new ValueCaster<>(valueClass, typeName); + byResult.putIfAbsent(valueClass, caster); + } +} diff --git a/src/main/java/carpet/script/annotation/ValueConverter.java b/src/main/java/carpet/script/annotation/ValueConverter.java new file mode 100644 index 0000000..8c916ef --- /dev/null +++ b/src/main/java/carpet/script/annotation/ValueConverter.java @@ -0,0 +1,219 @@ +package carpet.script.annotation; + +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.ParameterizedType; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang3.ClassUtils; + +import carpet.script.Context; +import carpet.script.annotation.Param.Params; +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +/** + *

Classes implementing this interface are able to convert {@link Value} instances into {@code }, in order to easily use them in parameters for + * Scarpet functions created using the {@link ScarpetFunction} annotation.

+ * + * @param The result type that the passed {@link Value}s will be converted to + */ +public interface ValueConverter +{ + /** + *

Returns the the user-friendly name of the result this {@link ValueConverter} converts to, without {@code a} or {@code an}, and without + * capitalizing the first letter.

+ * + *

This method can return {@code null}, in which case users of this function should hide this {@link ValueConverter} from any aids or usages of + * the function, meaning that the {@link ValueConverter} is only providing some meta information that isn't directly provided through the Scarpet + * language.

+ * + *

Those aids calling this method may append an {@code s} to the return value of this method, in case the type is used in places where more + * than one may be present, such as lists or varargs.

+ * + * @apiNote This method is intended to only be called when an error has occurred and therefore there is a need to print a stacktrace with some + * helpful usage instructions. + */ + @Nullable + String getTypeName(); + + /** + *

Converts the given {@link Value} to {@code }, which was defined when being registered.

+ * + *

Returns {@code null} if one of the conversions failed, either because the {@link Value} was incompatible in some position of the chain, or + * because the actual converting function returned {@code null} (which usually only occurs when the {@link Value} is incompatible/does not hold + * the appropriate information)

+ * + *

Functions using the converter can use {@link #getTypeName()} to get the name of the type this was trying to convert to, in case they are not + * trying to convert to anything else, where it would be recommended to tell the user the name of the final type instead.

+ * + * @param value The {@link Value} to convert + * @param context The {@link Context} of the call + * @return The converted value, or {@code null} if the conversion failed in the process + * @apiNote

While most implementations of this method should and will return the type from this method, implementations that require + * parameters from {@link #checkAndConvert(Iterator, Context, Context.Type)} or that require multiple parameters may decide to throw + * {@link UnsupportedOperationException} in this method and override {@link #checkAndConvert(Iterator, Context, Context.Type)} instead. Those + * implementations, however, should not be available for map or list types, since those can only operate with {@link Value}.

+ *

Currently, the only implementations requiring that are {@link Params#CONTEXT_PROVIDER} and {@link Params#CONTEXT_TYPE_PROVIDER}

+ *

Implementations can also provide different implementations for this and {@link #checkAndConvert(Iterator, Context, Context.Type)}, in case + * they can support it in some situations that can't be used else, such as inside of lists or maps, although they should try to provide + * in {@link #checkAndConvert(Iterator, Context, Context.Type)} at least the same conversion as the one from this method.

+ *

Even with the above reasons, {@link ValueConverter} users should try to implement {@link #convert(Value, Context)} whenever possible instead of + * {@link #checkAndConvert(Iterator, Context, Context.Type)}, since it allows its usage in generics of lists and maps.

+ */ + @Nullable R convert(Value value, @Nullable Context context); + + /** + * Old version of {@link #convert(Value)} without taking a {@link Context}.

+ * + * This shouldn't be used given converters now take a context in the convert function to allow for converting + * values in lists or other places without using static state.

+ * + * @param value The value to convert + * @return A converted value + * @deprecated Calling this method instead of {@link #convert(Value, Context)} may not return values for some converters + */ + @Nullable + @Deprecated(forRemoval = true) + default R convert(Value value) + { + try + { + return convert(value, null); + } + catch (NullPointerException e) + { + return null; + } + } + + /** + *

Returns whether this {@link ValueConverter} consumes a variable number of elements from the {@link Iterator} passed to it via + * {@link #checkAndConvert(Iterator, Context, Context.Type)}.

+ * + * @implNote The default implementation returns {@code false} + * @see #valueConsumption() + */ + default boolean consumesVariableArgs() + { + return false; + } + + /** + *

Declares the number of {@link Value}s this converter consumes from the {@link Iterator} passed to it in + * {@link #checkAndConvert(Iterator, Context, Context.Type)}.

+ * + *

If this {@link ValueConverter} can accept a variable number of arguments (therefore the result of calling {@link #consumesVariableArgs()} + * must return {@code true}), it will return the minimum number of arguments it will consume.

+ * + * @implNote The default implementation returns {@code 1} + * + */ + default int valueConsumption() + { + return 1; + } + + /** + *

Gets the proper {@link ValueConverter} for the given {@link AnnotatedType}, considering the type of {@code R[]} as {@code R}.

+ * + *

This function does not only consider the actual type (class) of the passed {@link AnnotatedType}, but also its annotations and generic + * parameters in order to get the most specific {@link ValueConverter}.

+ * + *

Some processing is delegated to the appropriate implementations of {@link ValueConverter} in order to get registered converters or generate + * specific ones for some functions.

+ * + * @param The type of the class the returned {@link ValueConverter} will convert to. It is declared from the type in the + * {@link AnnotatedType} directly inside the function. + * @param annoType The {@link AnnotatedType} to search a {@link ValueConverter}. + * @return A usable {@link ValueConverter} to convert from a {@link Value} to {@code } + */ + @SuppressWarnings("unchecked") + static ValueConverter fromAnnotatedType(AnnotatedType annoType) + { + Class type = annoType.getType() instanceof ParameterizedType ? // We are defining R here. + (Class) ((ParameterizedType) annoType.getType()).getRawType() : + (Class) annoType.getType(); + // I (altrisi) won't implement generics in varargs. Those are just PAINFUL. They have like 3-4 nested types and don't have the generics + // and annotations in the same place, plus they have a different "conversion hierarchy" than the rest, making everything require + // special methods to get the class from type, generics from type and annotations from type. Not worth the effort for me. + // Example: AnnotatedGenericTypeArray (or similar) being (@Paran.KeyValuePairs Map... name) + // Those will just fail with a ClassCastException. + if (type.isArray()) + { + type = (Class) type.getComponentType(); // Varargs + } + type = (Class) ClassUtils.primitiveToWrapper(type); // It will be boxed anyway, this saves unboxing-boxing + if (type == List.class) + { + return (ValueConverter) ListConverter.fromAnnotatedType(annoType); // Already checked that type is List + } + if (type == Map.class) + { + return (ValueConverter) MapConverter.fromAnnotatedType(annoType); // Already checked that type is Map + } + if (type == Optional.class) + { + return (ValueConverter) OptionalConverter.fromAnnotatedType(annoType); + } + if (annoType.getDeclaredAnnotations().length != 0) + { + if (annoType.isAnnotationPresent(Param.Custom.class)) + { + return Params.getCustomConverter(annoType, type); // Throws if incorrect usage + } + if (annoType.isAnnotationPresent(Param.Strict.class)) + { + return (ValueConverter) Params.getStrictConverter(annoType); // Throws if incorrect usage + } + if (annoType.getAnnotations()[0].annotationType().getEnclosingClass() == Locator.class) + { + return Locator.Locators.fromAnnotatedType(annoType, type); + } + } + + // Class only checks + if (Value.class.isAssignableFrom(type)) + { + return Objects.requireNonNull(ValueCaster.get(type), "Value subclass " + type + " is not registered. Register it in ValueCaster to use it"); + } + if (type == Context.class) + { + return (ValueConverter) Params.CONTEXT_PROVIDER; + } + if (type == Context.Type.class) + { + return (ValueConverter) Params.CONTEXT_TYPE_PROVIDER; + } + return Objects.requireNonNull(SimpleTypeConverter.get(type), "Type " + type + " is not registered. Register it in SimpleTypeConverter to use it"); + } + + /** + *

Checks for the presence of the next {@link Value} in the given {@link Iterator}, evaluates it with the given {@link Context} and then + * converts it to this {@link ValueConverter}'s output type.

+ * + *

This should be the preferred way to call the converter, since it allows multi-param converters and allows meta converters (such as the {@link Context} provider)

+ * + * @implSpec Implementations of this method are not required to move the {@link Iterator} to the next position, such as in the case of meta + * providers like {@link Context}.

Implementations can also use more than a single parameter when being called with this function, + * but in such case they must implement {@link #valueConsumption()} to return how many parameters do they consume at minimum, and, if + * they may consume variable arguments, implement {@link #consumesVariableArgs()}

This method holds the same nullability + * constraints as {@link #convert(Value, Context)}

+ * @param valueIterator An {@link Iterator} holding the {@link Value} to convert in next position + * @param context The {@link Context} this function has been called with + * @param contextType The {@link Context.Type} that the original function was called with + * @return The next {@link Value} (s) converted to the type {@code } of this {@link ValueConverter} + * @implNote This method's default implementation runs the {@link #convert(Value, Context)} function in the next {@link Value} ignoring {@link Context} and + * {@code theLazyT}. + */ + @Nullable + default R checkAndConvert(Iterator valueIterator, Context context, Context.Type contextType) + { + return !valueIterator.hasNext() ? null : convert(valueIterator.next(), context); + } +} diff --git a/src/main/java/carpet/script/annotation/package-info.java b/src/main/java/carpet/script/annotation/package-info.java new file mode 100644 index 0000000..2624cd0 --- /dev/null +++ b/src/main/java/carpet/script/annotation/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.annotation; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/api/Auxiliary.java b/src/main/java/carpet/script/api/Auxiliary.java new file mode 100644 index 0000000..ca3c22b --- /dev/null +++ b/src/main/java/carpet/script/api/Auxiliary.java @@ -0,0 +1,1392 @@ +package carpet.script.api; + +import carpet.script.external.Vanilla; +import carpet.script.utils.FeatureGenerator; +import carpet.script.argument.FileArgument; +import carpet.script.CarpetContext; +import carpet.script.CarpetEventServer; +import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.argument.BlockArgument; +import carpet.script.argument.FunctionArgument; +import carpet.script.argument.Vector3Argument; +import carpet.script.exception.ExitStatement; +import carpet.script.exception.InternalExpressionException; +import carpet.script.external.Carpet; +import carpet.script.utils.SnoopyCommandSource; +import carpet.script.utils.SystemInfo; +import carpet.script.utils.InputValidator; +import carpet.script.utils.ScarpetJsonDeserializer; +import carpet.script.utils.ShapeDispatcher; +import carpet.script.utils.WorldTools; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.LazyListValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import com.google.common.collect.Lists; +import net.minecraft.SharedConstants; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Rotations; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundClearTitlesPacket; +import net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.stats.Stat; +import net.minecraft.stats.StatType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.CommandStorage; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.phys.Vec3; +import org.apache.commons.io.file.PathUtils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; + +import javax.annotation.Nullable; +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; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class Auxiliary +{ + public static final String MARKER_STRING = "__scarpet_marker"; + private static final Map mixerMap = Arrays.stream(SoundSource.values()).collect(Collectors.toMap(SoundSource::getName, k -> k)); + public static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().registerTypeAdapter(Value.class, new ScarpetJsonDeserializer()).create(); + + @Deprecated + public static String recognizeResource(Value value, boolean isFloder) + { + String origfile = value.getString(); + String file = origfile.toLowerCase(Locale.ROOT).replaceAll("[^A-Za-z0-9\\-+_/]", ""); + file = Arrays.stream(file.split("/+")).filter(s -> !s.isEmpty()).collect(Collectors.joining("/")); + if (file.isEmpty() && !isFloder) + { + throw new InternalExpressionException("Cannot use " + origfile + " as resource name - must have some letters and numbers"); + } + return file; + } + + public static void apply(Expression expression) + { + expression.addContextFunction("sound", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + if (lv.isEmpty()) + { + return ListValue.wrap(cc.registry(Registries.SOUND_EVENT).holders().map(soundEventReference -> ValueConversions.of(soundEventReference.key().location()))); + } + String rawString = lv.get(0).getString(); + ResourceLocation soundName = InputValidator.identifierOf(rawString); + Vector3Argument locator = Vector3Argument.findIn(lv, 1); + + Holder soundHolder = Holder.direct(SoundEvent.createVariableRangeEvent(soundName)); + float volume = 1.0F; + float pitch = 1.0F; + SoundSource mixer = SoundSource.MASTER; + if (lv.size() > locator.offset) + { + volume = (float) NumericValue.asNumber(lv.get(locator.offset)).getDouble(); + if (lv.size() > 1 + locator.offset) + { + pitch = (float) NumericValue.asNumber(lv.get(1 + locator.offset)).getDouble(); + if (lv.size() > 2 + locator.offset) + { + String mixerName = lv.get(2 + locator.offset).getString(); + mixer = mixerMap.get(mixerName.toLowerCase(Locale.ROOT)); + if (mixer == null) + { + throw new InternalExpressionException(mixerName + " is not a valid mixer name"); + } + } + } + } + Vec3 vec = locator.vec; + double d0 = Math.pow(volume > 1.0F ? (double) (volume * 16.0F) : 16.0D, 2.0D); + int count = 0; + ServerLevel level = cc.level(); + long seed = level.getRandom().nextLong(); + for (ServerPlayer player : level.getPlayers(p -> p.distanceToSqr(vec) < d0)) + { + count++; + player.connection.send(new ClientboundSoundPacket(soundHolder, mixer, vec.x, vec.y, vec.z, volume, pitch, seed)); + } + return new NumericValue(count); + }); + + expression.addContextFunction("particle", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.isEmpty()) + { + return ListValue.wrap(cc.registry(Registries.PARTICLE_TYPE).holders().map(particleTypeReference -> ValueConversions.of(particleTypeReference.key().location()))); + } + MinecraftServer ms = cc.server(); + ServerLevel world = cc.level(); + Vector3Argument locator = Vector3Argument.findIn(lv, 1); + String particleName = lv.get(0).getString(); + int count = 10; + double speed = 0; + float spread = 0.5f; + ServerPlayer player = null; + if (lv.size() > locator.offset) + { + count = (int) NumericValue.asNumber(lv.get(locator.offset)).getLong(); + if (lv.size() > 1 + locator.offset) + { + spread = (float) NumericValue.asNumber(lv.get(1 + locator.offset)).getDouble(); + if (lv.size() > 2 + locator.offset) + { + speed = NumericValue.asNumber(lv.get(2 + locator.offset)).getDouble(); + if (lv.size() > 3 + locator.offset) // should accept entity as well as long as it is player + { + player = ms.getPlayerList().getPlayerByName(lv.get(3 + locator.offset).getString()); + } + } + } + } + ParticleOptions particle = ShapeDispatcher.getParticleData(particleName, world.registryAccess()); + Vec3 vec = locator.vec; + if (player == null) + { + for (ServerPlayer p : (world.players())) + { + world.sendParticles(p, particle, true, vec.x, vec.y, vec.z, count, + spread, spread, spread, speed); + } + } + else + { + world.sendParticles(player, + particle, true, vec.x, vec.y, vec.z, count, + spread, spread, spread, speed); + } + + return Value.TRUE; + }); + + expression.addContextFunction("particle_line", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerLevel world = cc.level(); + String particleName = lv.get(0).getString(); + ParticleOptions particle = ShapeDispatcher.getParticleData(particleName, world.registryAccess()); + Vector3Argument pos1 = Vector3Argument.findIn(lv, 1); + Vector3Argument pos2 = Vector3Argument.findIn(lv, pos1.offset); + double density = 1.0; + ServerPlayer player = null; + if (lv.size() > pos2.offset) + { + density = NumericValue.asNumber(lv.get(pos2.offset)).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); + if (playerValue instanceof EntityValue entityValue) + { + Entity e = entityValue.getEntity(); + if (!(e instanceof final ServerPlayer sp)) + { + throw new InternalExpressionException("'particle_line' player argument has to be a player"); + } + player = sp; + } + else + { + player = cc.server().getPlayerList().getPlayerByName(playerValue.getString()); + } + } + } + + return new NumericValue(ShapeDispatcher.drawParticleLine( + (player == null) ? world.players() : Collections.singletonList(player), + particle, pos1.vec, pos2.vec, density + )); + }); + + expression.addContextFunction("item_display_name", 1, (c, t, lv) -> new FormattedTextValue(ValueConversions.getItemStackFromValue(lv.get(0), false, ((CarpetContext) c).registryAccess()).getHoverName())); + + expression.addContextFunction("particle_box", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerLevel world = cc.level(); + String particleName = lv.get(0).getString(); + ParticleOptions particle = ShapeDispatcher.getParticleData(particleName, world.registryAccess()); + Vector3Argument pos1 = Vector3Argument.findIn(lv, 1); + Vector3Argument pos2 = Vector3Argument.findIn(lv, pos1.offset); + + double density = 1.0; + ServerPlayer player = null; + if (lv.size() > pos2.offset) + { + density = NumericValue.asNumber(lv.get(pos2.offset)).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); + if (playerValue instanceof EntityValue entityValue) + { + Entity e = entityValue.getEntity(); + if (!(e instanceof final ServerPlayer sp)) + { + throw new InternalExpressionException("'particle_box' player argument has to be a player"); + } + player = sp; + } + else + { + player = cc.server().getPlayerList().getPlayerByName(playerValue.getString()); + } + } + } + Vec3 a = pos1.vec; + Vec3 b = pos2.vec; + Vec3 from = new Vec3(min(a.x, b.x), min(a.y, b.y), min(a.z, b.z)); + Vec3 to = new Vec3(max(a.x, b.x), max(a.y, b.y), max(a.z, b.z)); + int particleCount = ShapeDispatcher.Box.particleMesh( + player == null ? world.players() : Collections.singletonList(player), + particle, density, from, to + ); + return new NumericValue(particleCount); + }); + // deprecated + expression.alias("particle_rect", "particle_box"); + + + expression.addContextFunction("draw_shape", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerLevel world = cc.level(); + MinecraftServer server = world.getServer(); + Set playerTargets = new HashSet<>(); + List shapes = new ArrayList<>(); + if (lv.size() == 1) // bulk + { + Value specLoad = lv.get(0); + if (!(specLoad instanceof final ListValue spec)) + { + throw new InternalExpressionException("In bulk mode - shapes need to be provided as a list of shape specs"); + } + for (Value list : spec.getItems()) + { + if (!(list instanceof final ListValue inner)) + { + throw new InternalExpressionException("In bulk mode - shapes need to be provided as a list of shape specs"); + } + shapes.add(ShapeDispatcher.fromFunctionArgs(server, world, inner.getItems(), playerTargets)); + } + } + else + { + shapes.add(ShapeDispatcher.fromFunctionArgs(server, world, lv, playerTargets)); + } + + ShapeDispatcher.sendShape( + (playerTargets.isEmpty()) ? cc.level().players() : playerTargets, + shapes, cc.registryAccess() + ); + return Value.TRUE; + }); + + expression.addContextFunction("create_marker", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + BlockState targetBlock = null; + Vector3Argument pointLocator; + boolean interactable = true; + Component name; + try + { + Value nameValue = lv.get(0); + name = nameValue.isNull() ? null : FormattedTextValue.getTextByValue(nameValue); + pointLocator = Vector3Argument.findIn(lv, 1, true, false); + if (lv.size() > pointLocator.offset) + { + BlockArgument blockLocator = BlockArgument.findIn(cc, lv, pointLocator.offset, true, true, false); + if (!(blockLocator instanceof BlockArgument.MissingBlockArgument)) + { + targetBlock = blockLocator.block.getBlockState(); + } + if (lv.size() > blockLocator.offset) + { + interactable = lv.get(blockLocator.offset).getBoolean(); + } + } + } + catch (IndexOutOfBoundsException e) + { + throw new InternalExpressionException("'create_marker' requires a name and three coordinates, with optional direction, and optional block on its head"); + } + Level level = cc.level(); + ArmorStand armorstand = new ArmorStand(EntityType.ARMOR_STAND, level); + double yoffset; + if (targetBlock == null && name == null) + { + yoffset = 0.0; + } + else if (!interactable && targetBlock == null) + { + yoffset = -0.41; + } + else + { + if (targetBlock == null) + { + yoffset = -armorstand.getBbHeight() - 0.41; + } + else + { + yoffset = -armorstand.getBbHeight() + 0.3; + } + } + armorstand.moveTo( + pointLocator.vec.x, + //pointLocator.vec.y - ((!interactable && targetBlock == null)?0.41f:((targetBlock==null)?(armorstand.getHeight()+0.41):(armorstand.getHeight()-0.3))), + pointLocator.vec.y + yoffset, + pointLocator.vec.z, + (float) pointLocator.yaw, + (float) pointLocator.pitch + ); + armorstand.addTag(MARKER_STRING + "_" + ((cc.host.getName() == null) ? "" : cc.host.getName())); + armorstand.addTag(MARKER_STRING); + if (targetBlock != null) + { + armorstand.setItemSlot(EquipmentSlot.HEAD, new ItemStack(targetBlock.getBlock().asItem())); + } + if (name != null) + { + armorstand.setCustomName(name); + armorstand.setCustomNameVisible(true); + } + armorstand.setHeadPose(new Rotations((int) pointLocator.pitch, 0, 0)); + armorstand.setNoGravity(true); + armorstand.setInvisible(true); + armorstand.setInvulnerable(true); + armorstand.getEntityData().set(ArmorStand.DATA_CLIENT_FLAGS, (byte) (interactable ? 8 : 16 | 8)); + level.addFreshEntity(armorstand); + return new EntityValue(armorstand); + }); + + expression.addContextFunction("remove_all_markers", 0, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + int total = 0; + String markerName = MARKER_STRING + "_" + ((cc.host.getName() == null) ? "" : cc.host.getName()); + for (Entity e : cc.level().getEntities(EntityType.ARMOR_STAND, as -> as.getTags().contains(markerName))) + { + total++; + e.discard(); + } + return new NumericValue(total); + }); + + expression.addUnaryFunction("nbt", NBTSerializableValue::fromValue); + + expression.addUnaryFunction("escape_nbt", v -> new StringValue(StringTag.quoteAndEscape(v.getString()))); + + expression.addUnaryFunction("parse_nbt", v -> { + if (v instanceof final NBTSerializableValue nbtsv) + { + return nbtsv.toValue(); + } + NBTSerializableValue ret = NBTSerializableValue.parseString(v.getString()); + return ret == null ? Value.NULL : ret.toValue(); + }); + + expression.addFunction("tag_matches", lv -> { + int numParam = lv.size(); + if (numParam != 2 && numParam != 3) + { + throw new InternalExpressionException("'tag_matches' requires 2 or 3 arguments"); + } + if (lv.get(1).isNull()) + { + return Value.TRUE; + } + if (lv.get(0).isNull()) + { + return Value.FALSE; + } + Tag source = ((NBTSerializableValue) (NBTSerializableValue.fromValue(lv.get(0)))).getTag(); + Tag match = ((NBTSerializableValue) (NBTSerializableValue.fromValue(lv.get(1)))).getTag(); + return BooleanValue.of(NbtUtils.compareNbt(match, source, numParam == 2 || lv.get(2).getBoolean())); + }); + + expression.addContextFunction("encode_nbt", -1, (c, t, lv) -> { + int argSize = lv.size(); + if (argSize == 0 || argSize > 2) + { + throw new InternalExpressionException("'encode_nbt' requires 1 or 2 parameters"); + } + Value v = lv.get(0); + boolean force = (argSize > 1) && lv.get(1).getBoolean(); + Tag tag; + try + { + tag = v.toTag(force, ((CarpetContext)c).registryAccess()); + } + catch (NBTSerializableValue.IncompatibleTypeException exception) + { + throw new InternalExpressionException("cannot reliably encode to a tag the value of '" + exception.val.getPrettyString() + "'"); + } + return new NBTSerializableValue(tag); + }); + + //"overridden" native call that prints to stderr + expression.addContextFunction("print", -1, (c, t, lv) -> + { + if (lv.isEmpty() || lv.size() > 2) + { + throw new InternalExpressionException("'print' takes one or two arguments"); + } + CarpetContext cc = (CarpetContext) c; + CommandSourceStack s = cc.source(); + MinecraftServer server = s.getServer(); + Value res = lv.get(0); + List targets = null; + if (lv.size() == 2) + { + List playerValues = (res instanceof ListValue list) ? list.getItems() : Collections.singletonList(res); + List playerTargets = new ArrayList<>(); + playerValues.forEach(pv -> { + ServerPlayer player = EntityValue.getPlayerByValue(server, pv); + if (player == null) + { + throw new InternalExpressionException("Cannot target player " + pv.getString() + " in print"); + } + playerTargets.add(player.createCommandSourceStack()); + }); + targets = playerTargets; + res = lv.get(1); + } else if (c.host.user != null) { + ServerPlayer player = cc.server().getPlayerList().getPlayerByName(cc.host.user); + if (player != null) { + targets = Collections.singletonList(player.createCommandSourceStack()); + } + } // optionally retrieve from CC.host.responsibleSource to print? + Component message = FormattedTextValue.getTextByValue(res); + if (targets == null) + { + s.sendSuccess(() -> message, false); + } + else + { + targets.forEach(p -> p.sendSuccess(() -> message, false)); + } + return res; // pass through for variables + }); + + expression.addContextFunction("display_title", -1, (c, t, lv) -> { + if (lv.size() < 2) + { + throw new InternalExpressionException("'display_title' needs at least a target, type and message, and optionally times"); + } + Value pVal = lv.get(0); + if (!(pVal instanceof ListValue)) + { + pVal = ListValue.of(pVal); + } + MinecraftServer server = ((CarpetContext) c).server(); + Stream targets = ((ListValue) pVal).getItems().stream().map(v -> + { + ServerPlayer player = EntityValue.getPlayerByValue(server, v); + if (player == null) + { + throw new InternalExpressionException("'display_title' requires a valid online player or a list of players as first argument. " + v.getString() + " is not a player."); + } + return player; + }); + Function> packetGetter = null; + String actionString = lv.get(1).getString().toLowerCase(Locale.ROOT); + switch (actionString) + { + case "title": + packetGetter = ClientboundSetTitleTextPacket::new; + if (lv.size() < 3) + { + throw new InternalExpressionException("Third argument of 'display_title' must be present except for 'clear' type"); + } + + break; + case "subtitle": + packetGetter = ClientboundSetSubtitleTextPacket::new; + if (lv.size() < 3) + { + throw new InternalExpressionException("Third argument of 'display_title' must be present except for 'clear' type"); + } + + break; + case "actionbar": + packetGetter = ClientboundSetActionBarTextPacket::new; + if (lv.size() < 3) + { + throw new InternalExpressionException("Third argument of 'display_title' must be present except for 'clear' type"); + } + + break; + case "clear": + packetGetter = x -> new ClientboundClearTitlesPacket(true); // resetting default fade + break; + case "player_list_header", "player_list_footer": + break; + default: + throw new InternalExpressionException("'display_title' requires 'title', 'subtitle', 'actionbar', 'player_list_header', 'player_list_footer' or 'clear' as second argument"); + } + Component title; + boolean soundsTrue = false; + if (lv.size() > 2) + { + pVal = lv.get(2); + title = FormattedTextValue.getTextByValue(pVal); + soundsTrue = pVal.getBoolean(); + } + else + { + title = null; // Will never happen, just to make lambda happy + } + if (packetGetter == null) + { + Map map; + if (actionString.equals("player_list_header")) + { + map = Carpet.getScarpetHeaders(); + } + else + { + map = Carpet.getScarpetFooters(); + } + + AtomicInteger total = new AtomicInteger(0); + List targetList = targets.collect(Collectors.toList()); + if (!soundsTrue) // null or empty string + { + targetList.forEach(target -> { + map.remove(target.getScoreboardName()); + total.getAndIncrement(); + }); + } + else + { + targetList.forEach(target -> { + map.put(target.getScoreboardName(), title); + total.getAndIncrement(); + }); + } + Carpet.updateScarpetHUDs(((CarpetContext) c).server(), targetList); + return NumericValue.of(total.get()); + } + ClientboundSetTitlesAnimationPacket timesPacket; // TimesPacket + if (lv.size() > 3) + { + if (lv.size() != 6) + { + throw new InternalExpressionException("'display_title' needs all fade-in, stay and fade-out times"); + } + int in = NumericValue.asNumber(lv.get(3), "fade in for display_title").getInt(); + int stay = NumericValue.asNumber(lv.get(4), "stay for display_title").getInt(); + int out = NumericValue.asNumber(lv.get(5), "fade out for display_title").getInt(); + timesPacket = new ClientboundSetTitlesAnimationPacket(in, stay, out); + } + else + { + timesPacket = null; + } + + Packet packet = packetGetter.apply(title); + AtomicInteger total = new AtomicInteger(0); + targets.forEach(p -> { + if (timesPacket != null) + { + p.connection.send(timesPacket); + } + p.connection.send(packet); + total.getAndIncrement(); + }); + return NumericValue.of(total.get()); + }); + + expression.addFunction("format", values -> { + if (values.isEmpty()) + { + throw new InternalExpressionException("'format' requires at least one component"); + } + if (values.get(0) instanceof final ListValue list && values.size() == 1) + { + values = list.getItems(); + } + return new FormattedTextValue(Carpet.Messenger_compose(values.stream().map(Value::getString).toArray())); + }); + + expression.addContextFunction("run", 1, (c, t, lv) -> + { + CommandSourceStack s = ((CarpetContext) c).source(); + try + { + Component[] error = {null}; + List output = new ArrayList<>(); + s.getServer().getCommands().performPrefixedCommand( + new SnoopyCommandSource(s, error, output), + lv.get(0).getString()); + return ListValue.of( + NumericValue.ZERO, + ListValue.wrap(output.stream().map(FormattedTextValue::new)), + FormattedTextValue.of(error[0]) + ); + } + catch (Exception exc) + { + return ListValue.of(Value.NULL, ListValue.of(), new FormattedTextValue(Component.literal(exc.getMessage()))); + } + }); + + expression.addContextFunction("save", 0, (c, t, lv) -> + { + CommandSourceStack s = ((CarpetContext) c).source(); + s.getServer().getPlayerList().saveAll(); + s.getServer().saveAllChunks(true, true, true); + for (ServerLevel world : s.getServer().getAllLevels()) + { + world.getChunkSource().tick(() -> true, false); + } + CarpetScriptServer.LOG.warn("Saved chunks"); + return Value.TRUE; + }); + + expression.addContextFunction("tick_time", 0, (c, t, lv) -> + new NumericValue(((CarpetContext) c).server().getTickCount())); + + expression.addContextFunction("world_time", 0, (c, t, lv) -> { + c.host.issueDeprecation("world_time()"); + return new NumericValue(((CarpetContext) c).level().getGameTime()); + }); + + expression.addContextFunction("day_time", -1, (c, t, lv) -> + { + Value time = new NumericValue(((CarpetContext) c).level().getDayTime()); + if (!lv.isEmpty()) + { + long newTime = NumericValue.asNumber(lv.get(0)).getLong(); + if (newTime < 0) + { + newTime = 0; + } + ((CarpetContext) c).level().setDayTime(newTime); + } + return time; + }); + + expression.addContextFunction("last_tick_times", -1, (c, t, lv) -> + { + c.host.issueDeprecation("last_tick_times()"); + return SystemInfo.get("server_last_tick_times", (CarpetContext) c); + }); + + + expression.addContextFunction("game_tick", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + MinecraftServer server = cc.server(); + CarpetScriptServer scriptServer = (CarpetScriptServer) c.host.scriptServer(); + if (scriptServer == null) + { + return Value.NULL; + } + if (!server.isSameThread()) + { + throw new InternalExpressionException("Unable to run ticks from threads"); + } + if (scriptServer.tickDepth > 16) + { + throw new InternalExpressionException("'game_tick' function caused other 'game_tick' functions to run. You should not allow that."); + } + try + { + scriptServer.tickDepth++; + Vanilla.MinecraftServer_forceTick(server, () -> System.nanoTime() - scriptServer.tickStart < 50000000L); + if (!lv.isEmpty()) + { + long msTotal = NumericValue.asNumber(lv.get(0)).getLong(); + long endExpected = scriptServer.tickStart + msTotal * 1000000L; + long wait = endExpected - System.nanoTime(); + if (wait > 0L) + { + try + { + Thread.sleep(wait / 1000000L); + } + catch (InterruptedException ignored) + { + } + } + } + scriptServer.tickStart = System.nanoTime(); // for the next tick + Thread.yield(); + } + finally + { + if (!scriptServer.stopAll) + { + scriptServer.tickDepth--; + } + } + if (scriptServer.stopAll) + { + throw new ExitStatement(Value.NULL); + } + return Value.TRUE; + }); + + expression.addContextFunction("seed", -1, (c, t, lv) -> { + CommandSourceStack s = ((CarpetContext) c).source(); + c.host.issueDeprecation("seed()"); + return new NumericValue(s.getLevel().getSeed()); + }); + + expression.addContextFunction("relight", -1, (c, t, lv) -> + { + return Value.NULL; + /* + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + BlockPos pos = locator.block.getPos(); + ServerLevel world = cc.level(); + Vanilla.ChunkMap_relightChunk(world.getChunkSource().chunkMap, new ChunkPos(pos)); + WorldTools.forceChunkUpdate(pos, world); + return Value.TRUE; + + */ + }); + + // Should this be deprecated for system_info('source_dimension')? + expression.addContextFunction("current_dimension", 0, (c, t, lv) -> + ValueConversions.of(((CarpetContext) c).level())); + + expression.addContextFunction("view_distance", 0, (c, t, lv) -> { + c.host.issueDeprecation("view_distance()"); + return new NumericValue(((CarpetContext) c).server().getPlayerList().getViewDistance()); + }); + + // lazy due to passthrough and context changing ability + expression.addLazyFunction("in_dimension", 2, (c, t, lv) -> { + CommandSourceStack outerSource = ((CarpetContext) c).source(); + Value dimensionValue = lv.get(0).evalValue(c); + Level world = ValueConversions.dimFromValue(dimensionValue, outerSource.getServer()); + if (world == outerSource.getLevel()) + { + return lv.get(1); + } + CommandSourceStack innerSource = outerSource.withLevel((ServerLevel) world); + Context newCtx = c.recreate(); + ((CarpetContext) newCtx).swapSource(innerSource); + newCtx.variables = c.variables; + Value retval = lv.get(1).evalValue(newCtx); + return (cc, tt) -> retval; + }); + + expression.addContextFunction("plop", -1, (c, t, lv) -> { + if (lv.isEmpty()) + { + Map plopData = new HashMap<>(); + CarpetContext cc = (CarpetContext) c; + plopData.put(StringValue.of("scarpet_custom"), + ListValue.wrap(FeatureGenerator.featureMap.keySet().stream().sorted().map(StringValue::of)) + ); + plopData.put(StringValue.of("features"), + ListValue.wrap(cc.registry(Registries.FEATURE).keySet().stream().sorted().map(ValueConversions::of)) + ); + plopData.put(StringValue.of("configured_features"), + ListValue.wrap(cc.registry(Registries.CONFIGURED_FEATURE).keySet().stream().sorted().map(ValueConversions::of)) + ); + plopData.put(StringValue.of("structure_types"), + ListValue.wrap(cc.registry(Registries.STRUCTURE_TYPE).keySet().stream().sorted().map(ValueConversions::of)) + ); + plopData.put(StringValue.of("structures"), + ListValue.wrap(cc.registry(Registries.STRUCTURE).keySet().stream().sorted().map(ValueConversions::of)) + ); + return MapValue.wrap(plopData); + } + 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).getString(); + Value[] result = new Value[]{Value.NULL}; + ((CarpetContext) c).server().executeBlocking(() -> + { + Boolean res = FeatureGenerator.plop(what, ((CarpetContext) c).level(), locator.block.getPos()); + + if (res == null) + { + return; + } + result[0] = BooleanValue.of(res); + }); + return result[0]; + }); + + expression.addContextFunction("schedule", -1, (c, t, lv) -> { + if (lv.size() < 2) + { + throw new InternalExpressionException("'schedule' should have at least 2 arguments, delay and call name"); + } + long delay = NumericValue.asNumber(lv.get(0)).getLong(); + + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 1, false, false); + ((CarpetScriptServer)c.host.scriptServer()).events.scheduleCall( + (CarpetContext) c, + functionArgument.function, + functionArgument.checkedArgs(), + delay + ); + return Value.TRUE; + }); + + expression.addImpureFunction("logger", lv -> + { + Value res; + + if (lv.size() == 1) + { + res = lv.get(0); + CarpetScriptServer.LOG.info(res.getString()); + } + else if (lv.size() == 2) + { + String level = lv.get(0).getString().toLowerCase(Locale.ROOT); + res = lv.get(1); + switch (level) + { + case "debug" -> CarpetScriptServer.LOG.debug(res.getString()); + case "warn" -> CarpetScriptServer.LOG.warn(res.getString()); + case "info" -> CarpetScriptServer.LOG.info(res.getString()); + // Somehow issue deprecation + case "fatal", "error" -> CarpetScriptServer.LOG.error(res.getString()); + default -> throw new InternalExpressionException("Unknown log level for 'logger': " + level); + } + } + else + { + throw new InternalExpressionException("logger takes 1 or 2 arguments"); + } + + return res; // pass through for variables + }); + + expression.addContextFunction("list_files", 2, (c, t, lv) -> + { + FileArgument fdesc = FileArgument.from(c, lv, true, FileArgument.Reason.READ); + Stream files = ((CarpetScriptHost) c.host).listFolder(fdesc); + return files == null ? Value.NULL : ListValue.wrap(files.map(StringValue::of)); + }); + + expression.addContextFunction("read_file", 2, (c, t, lv) -> + { + FileArgument fdesc = FileArgument.from(c, lv, false, FileArgument.Reason.READ); + if (fdesc.type == FileArgument.Type.NBT) + { + Tag state = ((CarpetScriptHost) c.host).readFileTag(fdesc); + return state == null ? Value.NULL : new NBTSerializableValue(state); + } + else if (fdesc.type == FileArgument.Type.JSON) + { + JsonElement json; + json = ((CarpetScriptHost) c.host).readJsonFile(fdesc); + Value parsedJson = GSON.fromJson(json, Value.class); + return parsedJson == null ? Value.NULL : parsedJson; + } + else + { + List content = ((CarpetScriptHost) c.host).readTextResource(fdesc); + return content == null ? Value.NULL : ListValue.wrap(content.stream().map(StringValue::new)); + } + }); + + expression.addContextFunction("delete_file", 2, (c, t, lv) -> + BooleanValue.of(((CarpetScriptHost) c.host).removeResourceFile(FileArgument.from(c, lv, false, FileArgument.Reason.DELETE)))); + + expression.addContextFunction("write_file", -1, (c, t, lv) -> { + if (lv.size() < 3) + { + throw new InternalExpressionException("'write_file' requires three or more arguments"); + } + FileArgument fdesc = FileArgument.from(c, lv, false, FileArgument.Reason.CREATE); + + boolean success; + if (fdesc.type == FileArgument.Type.NBT) + { + Value val = lv.get(2); + NBTSerializableValue tagValue = (val instanceof final NBTSerializableValue nbtsv) + ? nbtsv + : new NBTSerializableValue(val.getString()); + Tag tag = tagValue.getTag(); + success = ((CarpetScriptHost) c.host).writeTagFile(tag, fdesc); + } + else if (fdesc.type == FileArgument.Type.JSON) + { + List data = Collections.singletonList(GSON.toJson(lv.get(2).toJson())); + ((CarpetScriptHost) c.host).removeResourceFile(fdesc); + success = ((CarpetScriptHost) c.host).appendLogFile(fdesc, data); + } + else + { + List data = new ArrayList<>(); + if (lv.size() == 3) + { + Value val = lv.get(2); + if (val instanceof final ListValue list) + { + List lval = list.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).getString()); + } + } + success = ((CarpetScriptHost) c.host).appendLogFile(fdesc, data); + } + return BooleanValue.of(success); + }); + + expression.addContextFunction("load_app_data", -1, (c, t, lv) -> + { + FileArgument fdesc = new FileArgument(null, FileArgument.Type.NBT, null, false, false, FileArgument.Reason.READ, c.host); + if (!lv.isEmpty()) + { + c.host.issueDeprecation("load_app_data(...) with arguments"); + String resource = recognizeResource(lv.get(0), false); + boolean shared = lv.size() > 1 && lv.get(1).getBoolean(); + fdesc = new FileArgument(resource, FileArgument.Type.NBT, null, false, shared, FileArgument.Reason.READ, c.host); + } + return NBTSerializableValue.of(((CarpetScriptHost) c.host).readFileTag(fdesc)); + }); + + expression.addContextFunction("store_app_data", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'store_app_data' needs NBT tag and an optional file"); + } + Value val = lv.get(0); + FileArgument fdesc = new FileArgument(null, FileArgument.Type.NBT, null, false, false, FileArgument.Reason.CREATE, c.host); + if (lv.size() > 1) + { + c.host.issueDeprecation("store_app_data(...) with more than one argument"); + String resource = recognizeResource(lv.get(1), false); + boolean shared = lv.size() > 2 && lv.get(2).getBoolean(); + fdesc = new FileArgument(resource, FileArgument.Type.NBT, null, false, shared, FileArgument.Reason.CREATE, c.host); + } + NBTSerializableValue tagValue = (val instanceof final NBTSerializableValue nbtsv) + ? nbtsv + : new NBTSerializableValue(val.getString()); + return BooleanValue.of(((CarpetScriptHost) c.host).writeTagFile(tagValue.getTag(), fdesc)); + }); + + expression.addContextFunction("statistic", 3, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerPlayer player = EntityValue.getPlayerByValue(cc.server(), lv.get(0)); + if (player == null) + { + return Value.NULL; + } + ResourceLocation category; + ResourceLocation statName; + category = InputValidator.identifierOf(lv.get(1).getString()); + statName = InputValidator.identifierOf(lv.get(2).getString()); + StatType type = cc.registry(Registries.STAT_TYPE).get(category); + if (type == null) + { + return Value.NULL; + } + Stat stat = getStat(type, statName); + if (stat == null) + { + return Value.NULL; + } + return new NumericValue(player.getStats().getValue(stat)); + }); + + //handle_event('event', function...) + expression.addContextFunction("handle_event", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'handle_event' requires at least two arguments, event name, and a callback"); + } + String event = lv.get(0).getString(); + FunctionArgument callback = FunctionArgument.findIn(c, expression.module, lv, 1, true, false); + CarpetScriptHost host = ((CarpetScriptHost) c.host); + if (callback.function == null) + { + return BooleanValue.of(host.scriptServer().events.removeBuiltInEvent(event, host)); + } + // args don't need to be checked will be checked at the event + return BooleanValue.of(host.scriptServer().events.handleCustomEvent(event, host, callback.function, callback.args)); + }); + //signal_event('event', player or null, args.... ) -> number of apps notified + expression.addContextFunction("signal_event", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'signal' requires at least one argument"); + } + CarpetContext cc = (CarpetContext) c; + CarpetScriptServer server = ((CarpetScriptHost) c.host).scriptServer(); + String eventName = lv.get(0).getString(); + // no such event yet + if (CarpetEventServer.Event.getEvent(eventName, server) == null) + { + return Value.NULL; + } + ServerPlayer player = null; + List args = Collections.emptyList(); + if (lv.size() > 1) + { + player = EntityValue.getPlayerByValue(server.server, lv.get(1)); + if (lv.size() > 2) + { + args = lv.subList(2, lv.size()); + } + } + int counts = ((CarpetScriptHost) c.host).scriptServer().events.signalEvent(eventName, cc, player, args); + if (counts < 0) + { + return Value.NULL; + } + return new NumericValue(counts); + }); + + // nbt_storage() + // nbt_storage(key) + // nbt_storage(key, nbt) + expression.addContextFunction("nbt_storage", -1, (c, t, lv) -> { + if (lv.size() > 2) + { + throw new InternalExpressionException("'nbt_storage' requires 0, 1 or 2 arguments."); + } + CarpetContext cc = (CarpetContext) c; + CommandStorage storage = cc.server().getCommandStorage(); + if (lv.isEmpty()) + { + return ListValue.wrap(storage.keys().map(NBTSerializableValue::nameFromRegistryId)); + } + String key = lv.get(0).getString(); + CompoundTag oldNbt = storage.get(InputValidator.identifierOf(key)); + if (lv.size() == 2) + { + Value nbt = lv.get(1); + NBTSerializableValue newNbt = (nbt instanceof final NBTSerializableValue nbtsv) + ? nbtsv + : NBTSerializableValue.parseStringOrFail(nbt.getString()); + storage.set(InputValidator.identifierOf(key), newNbt.getCompoundTag()); + } + return NBTSerializableValue.of(oldNbt); + }); + + // script run create_datapack('foo', {'foo' -> {'bar.json' -> {'c' -> true,'d' -> false,'e' -> {'foo' -> [1,2,3]},'a' -> 'foobar','b' -> 5}}}) + expression.addContextFunction("create_datapack", 2, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + String origName = lv.get(0).getString(); + String name = InputValidator.validateSimpleString(origName, true); + MinecraftServer server = cc.server(); + for (String dpName : server.getPackRepository().getAvailableIds()) + { + if (dpName.equalsIgnoreCase("file/" + name + ".zip") || + dpName.equalsIgnoreCase("file/" + name)) + { + return Value.NULL; + } + + } + Value dpdata = lv.get(1); + if (!(dpdata instanceof final MapValue dpMap)) + { + throw new InternalExpressionException("datapack data needs to be a valid map type"); + } + PackRepository packManager = server.getPackRepository(); + Path dbFloder = server.getWorldPath(LevelResource.DATAPACK_DIR); + Path packFloder = dbFloder.resolve(name + ".zip"); + if (Files.exists(packFloder) || Files.exists(dbFloder.resolve(name))) + { + return Value.NULL; + } + Boolean[] successful = new Boolean[]{true}; + server.executeBlocking(() -> + { + try + { + try (FileSystem zipfs = FileSystems.newFileSystem(URI.create("jar:" + packFloder.toUri()), Map.of("create", "true"))) + { + Path zipRoot = zipfs.getPath("/"); + zipValueToJson(zipRoot.resolve("pack.mcmeta"), MapValue.wrap( + Map.of(StringValue.of("pack"), MapValue.wrap(Map.of( + StringValue.of("pack_format"), new NumericValue(SharedConstants.getCurrentVersion().getPackVersion(PackType.SERVER_DATA)), + StringValue.of("description"), StringValue.of(name), + StringValue.of("source"), StringValue.of("scarpet") + ))) + )); + walkTheDPMap(dpMap, zipRoot); + } + packManager.reload(); + Pack resourcePackProfile = packManager.getPack("file/" + name + ".zip"); + if (resourcePackProfile == null || packManager.getSelectedPacks().contains(resourcePackProfile)) + { + throw new IOException(); + } + List list = Lists.newArrayList(packManager.getSelectedPacks()); + resourcePackProfile.getDefaultPosition().insert(list, resourcePackProfile, Pack::selectionConfig, false); + + + server.reloadResources(list.stream().map(Pack::getId).collect(Collectors.toList())). + exceptionally(exc -> { + successful[0] = false; + return null; + }).join(); + if (!successful[0]) + { + throw new IOException(); + } + } + catch (IOException e) + { + successful[0] = false; + try + { + PathUtils.delete(packFloder); + } + catch (IOException ignored) + { + throw new InternalExpressionException("Failed to install a datapack and failed to clean up after it"); + } + + } + }); + return BooleanValue.of(successful[0]); + }); + + expression.addContextFunction("enable_hidden_dimensions", 0, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + cc.host.issueDeprecation("enable_hidden_dimensions in 1.18.2 and 1.19+"); + return Value.NULL; + }); + } + + 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 zipValueToText(Path path, Value output) throws IOException + { + List toJoin; + String string; + String delimiter = System.lineSeparator(); + // i dont know it shoule be \n or System.lineSeparator + if (output instanceof LazyListValue lazyListValue) + { + toJoin = lazyListValue.unroll(); + string = toJoin.stream().map(Value::getString).collect(Collectors.joining(delimiter)); + } + else if (output instanceof ListValue listValue) + { + toJoin = listValue.getItems(); + string = toJoin.stream().map(Value::getString).collect(Collectors.joining(delimiter)); + } + else + { + string = output.getString(); + } + + + 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 zipValueToNBT(Path path, Value output) throws IOException + { + NBTSerializableValue tagValue = (output instanceof NBTSerializableValue nbtSerializableValue) + ? nbtSerializableValue + : new NBTSerializableValue(output.getString()); + Tag tag = tagValue.getTag(); + Files.createDirectories(path.getParent()); + if (tag instanceof final CompoundTag cTag) + { + NbtIo.writeCompressed(cTag, Files.newOutputStream(path)); + } + } + + private static void walkTheDPMap(MapValue node, Path path) throws IOException + { + Map items = node.getMap(); + for (Map.Entry 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 (strkey.endsWith(".mcfunction") || strkey.endsWith(".txt") || strkey.endsWith(".mcmeta")) + { + zipValueToText(child, val); + } + else if (strkey.endsWith(".nbt")) + { + zipValueToNBT(child, val); + } + else + { + if (!(val instanceof final MapValue map)) + { + throw new InternalExpressionException("Value of " + strkey + " should be a map"); + } + Files.createDirectory(child); + walkTheDPMap(map, child); + } + } + } + + @Nullable + private static Stat getStat(StatType type, ResourceLocation id) + { + T key = type.getRegistry().get(id); + if (key == null || !type.contains(key)) + { + return null; + } + return type.get(key); + } +} diff --git a/src/main/java/carpet/script/api/BlockIterators.java b/src/main/java/carpet/script/api/BlockIterators.java new file mode 100644 index 0000000..d16c373 --- /dev/null +++ b/src/main/java/carpet/script/api/BlockIterators.java @@ -0,0 +1,545 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Fluff; +import carpet.script.LazyValue; +import carpet.script.argument.BlockArgument; +import carpet.script.argument.Vector3Argument; +import carpet.script.exception.BreakStatement; +import carpet.script.exception.ContinueStatement; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.BlockValue; +import carpet.script.value.LazyListValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; + +import static java.lang.Math.abs; +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class BlockIterators +{ + public static void apply(Expression expression) + { + // lazy cause of lazy expression + expression.addLazyFunction("scan", (c, t, llv) -> + { + if (llv.size() < 3) + { + throw new InternalExpressionException("'scan' needs many more arguments"); + } + List lv = Fluff.AbstractFunction.unpackLazy(llv.subList(0, llv.size() - 1), c, Context.NONE); + CarpetContext cc = (CarpetContext) c; + BlockArgument centerLocator = BlockArgument.findIn(cc, lv, 0); + Vector3Argument rangeLocator = Vector3Argument.findIn(lv, centerLocator.offset); + BlockPos center = centerLocator.block.getPos(); + Vec3i range; + + if (rangeLocator.fromBlock) + { + range = new Vec3i( + Mth.floor(abs(rangeLocator.vec.x - center.getX())), + Mth.floor(abs(rangeLocator.vec.y - center.getY())), + Mth.floor(abs(rangeLocator.vec.z - center.getZ())) + ); + } + else + { + range = new Vec3i( + Mth.floor(abs(rangeLocator.vec.x)), + Mth.floor(abs(rangeLocator.vec.y)), + Mth.floor(abs(rangeLocator.vec.z)) + ); + } + Vec3i upperRange = range; + if (lv.size() > rangeLocator.offset + 1) // +1 cause we still need the expression + { + rangeLocator = Vector3Argument.findIn(lv, rangeLocator.offset); + if (rangeLocator.fromBlock) + { + upperRange = new Vec3i( + Mth.floor(abs(rangeLocator.vec.x - center.getX())), + Mth.floor(abs(rangeLocator.vec.y - center.getY())), + Mth.floor(abs(rangeLocator.vec.z - center.getZ())) + ); + } + else + { + upperRange = new Vec3i( + Mth.floor(abs(rangeLocator.vec.x)), + Mth.floor(abs(rangeLocator.vec.y)), + Mth.floor(abs(rangeLocator.vec.z))); + } + } + if (llv.size() != rangeLocator.offset + 1) + { + throw new InternalExpressionException("'scan' takes two, or three block positions, and an expression: " + lv.size() + " " + rangeLocator.offset); + } + LazyValue expr = llv.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 xVal = c.getVariable("_x"); + LazyValue yVal = c.getVariable("_y"); + LazyValue zVal = c.getVariable("_z"); + LazyValue defaultVal = c.getVariable("_"); + int sCount = 0; + outer: + for (int y = cy - yrange; y <= cy + yprange; y++) + { + int yFinal = y; + c.setVariable("_y", (ct, tt) -> new NumericValue(yFinal).bindTo("_y")); + for (int x = cx - xrange; x <= cx + xprange; x++) + { + int xFinal = x; + c.setVariable("_x", (ct, tt) -> new NumericValue(xFinal).bindTo("_x")); + for (int z = cz - zrange; z <= cz + zprange; z++) + { + int zFinal = z; + + c.setVariable("_z", (ct, tt) -> new NumericValue(zFinal).bindTo("_z")); + Value blockValue = BlockValue.fromCoords(((CarpetContext) c), xFinal, yFinal, zFinal).bindTo("_"); + c.setVariable("_", (ct, tt) -> blockValue); + Value result; + try + { + result = expr.evalValue(c, t); + } + catch (ContinueStatement notIgnored) + { + result = notIgnored.retval; + } + catch (BreakStatement notIgnored) + { + break outer; + } + if (t != Context.VOID && result.getBoolean()) + { + sCount += 1; + } + } + } + } + //restoring outer scope + c.setVariable("_x", xVal); + c.setVariable("_y", yVal); + c.setVariable("_z", zVal); + c.setVariable("_", defaultVal); + int finalSCount = sCount; + return (ct, tt) -> new NumericValue(finalSCount); + }); + + // must be lazy + expression.addLazyFunction("volume", (c, t, llv) -> + { + CarpetContext cc = (CarpetContext) c; + if (llv.size() < 3) + { + throw new InternalExpressionException("'volume' needs many more arguments"); + } + List lv = Fluff.AbstractFunction.unpackLazy(llv.subList(0, llv.size() - 1), c, Context.NONE); + + BlockArgument pos1Locator = BlockArgument.findIn(cc, lv, 0); + BlockArgument pos2Locator = BlockArgument.findIn(cc, lv, pos1Locator.offset); + BlockPos pos1 = pos1Locator.block.getPos(); + BlockPos pos2 = pos2Locator.block.getPos(); + + int x1 = pos1.getX(); + int y1 = pos1.getY(); + int z1 = pos1.getZ(); + int x2 = pos2.getX(); + int y2 = pos2.getY(); + int z2 = pos2.getZ(); + int minx = min(x1, x2); + int miny = min(y1, y2); + int minz = min(z1, z2); + int maxx = max(x1, x2); + int maxy = max(y1, y2); + int maxz = max(z1, z2); + LazyValue expr = llv.get(pos2Locator.offset); + + //saving outer scope + LazyValue xVal = c.getVariable("_x"); + LazyValue yVal = c.getVariable("_y"); + LazyValue zVal = c.getVariable("_z"); + LazyValue defaultVal = c.getVariable("_"); + int sCount = 0; + outer: + for (int y = miny; y <= maxy; y++) + { + int yFinal = y; + c.setVariable("_y", (ct, tt) -> new NumericValue(yFinal).bindTo("_y")); + for (int x = minx; x <= maxx; x++) + { + int xFinal = x; + c.setVariable("_x", (ct, tt) -> new NumericValue(xFinal).bindTo("_x")); + for (int z = minz; z <= maxz; z++) + { + int zFinal = z; + c.setVariable("_z", (ct, tt) -> new NumericValue(zFinal).bindTo("_z")); + Value blockValue = BlockValue.fromCoords(((CarpetContext) c), xFinal, yFinal, zFinal).bindTo("_"); + c.setVariable("_", (ct, tt) -> blockValue); + Value result; + try + { + result = expr.evalValue(c, t); + } + catch (ContinueStatement notIgnored) + { + result = notIgnored.retval; + } + catch (BreakStatement notIgnored) + { + break outer; + } + if (t != Context.VOID && result.getBoolean()) + { + sCount += 1; + } + } + } + } + //restoring outer scope + c.setVariable("_x", xVal); + c.setVariable("_y", yVal); + c.setVariable("_z", zVal); + c.setVariable("_", defaultVal); + int finalSCount = sCount; + return (ct, tt) -> new NumericValue(finalSCount); + }); + + expression.addContextFunction("neighbours", -1, (c, t, lv) -> + { + BlockPos center = BlockArgument.findIn((CarpetContext) c, lv, 0).block.getPos(); + ServerLevel world = ((CarpetContext) c).level(); + + List neighbours = new ArrayList<>(); + neighbours.add(new BlockValue(world, center.above())); + neighbours.add(new BlockValue(world, center.below())); + neighbours.add(new BlockValue(world, center.north())); + neighbours.add(new BlockValue(world, center.south())); + neighbours.add(new BlockValue(world, center.east())); + neighbours.add(new BlockValue(world, center.west())); + return ListValue.wrap(neighbours); + }); + + expression.addContextFunction("rect", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + int cx; + int cy; + int cz; + int sminx; + int sminy; + int sminz; + int smaxx; + int smaxy; + int smaxz; + BlockArgument cposLocator = BlockArgument.findIn(cc, lv, 0); + BlockPos cpos = cposLocator.block.getPos(); + cx = cpos.getX(); + cy = cpos.getY(); + cz = cpos.getZ(); + if (lv.size() > cposLocator.offset) + { + Vector3Argument diffLocator = Vector3Argument.findIn(lv, cposLocator.offset); + if (diffLocator.fromBlock) + { + sminx = Mth.floor(abs(diffLocator.vec.x - cx)); + sminy = Mth.floor(abs(diffLocator.vec.y - cx)); + sminz = Mth.floor(abs(diffLocator.vec.z - cx)); + } + else + { + sminx = Mth.floor(abs(diffLocator.vec.x)); + sminy = Mth.floor(abs(diffLocator.vec.y)); + sminz = Mth.floor(abs(diffLocator.vec.z)); + } + if (lv.size() > diffLocator.offset) + { + Vector3Argument posDiff = Vector3Argument.findIn(lv, diffLocator.offset); + if (posDiff.fromBlock) + { + smaxx = Mth.floor(abs(posDiff.vec.x - cx)); + smaxy = Mth.floor(abs(posDiff.vec.y - cx)); + smaxz = Mth.floor(abs(posDiff.vec.z - cx)); + } + else + { + smaxx = Mth.floor(abs(posDiff.vec.x)); + smaxy = Mth.floor(abs(posDiff.vec.y)); + smaxz = Mth.floor(abs(posDiff.vec.z)); + } + } + else + { + smaxx = sminx; + smaxy = sminy; + smaxz = sminz; + } + } + else + { + sminx = 1; + sminy = 1; + sminz = 1; + smaxx = 1; + smaxy = 1; + smaxz = 1; + } + + return new LazyListValue() + { + final int minx = cx - sminx; + final int miny = cy - sminy; + final int minz = cz - sminz; + final int maxx = cx + smaxx; + final int maxy = cy + smaxy; + final int maxz = cz + smaxz; + + int x; + int y; + int z; + + { + reset(); + } + + @Override + public boolean hasNext() + { + return y <= maxy; + } + + @Override + public Value next() + { + Value r = BlockValue.fromCoords(cc, x, y, z); + //possibly reroll context + x++; + if (x > maxx) + { + x = minx; + z++; + if (z > maxz) + { + z = minz; + y++; + // hasNext should fail if we went over + } + } + + return r; + } + + @Override + public void fatality() + { + // possibly return original x, y, z + super.fatality(); + } + + @Override + public void reset() + { + x = minx; + y = miny; + z = minz; + } + + @Override + public String getString() + { + return String.format(Locale.ROOT, "rect[(%d,%d,%d),..,(%d,%d,%d)]", minx, miny, minz, maxx, maxy, maxz); + } + }; + }); + + expression.addContextFunction("diamond", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + + BlockArgument cposLocator = BlockArgument.findIn((CarpetContext) c, lv, 0); + BlockPos cpos = cposLocator.block.getPos(); + + int cx; + int cy; + int cz; + int width; + int height; + try + { + cx = cpos.getX(); + cy = cpos.getY(); + cz = cpos.getZ(); + + if (lv.size() == cposLocator.offset) + { + return ListValue.of( + BlockValue.fromCoords(cc, cx, cy - 1, cz), + BlockValue.fromCoords(cc, cx, cy, cz), + BlockValue.fromCoords(cc, cx - 1, cy, cz), + BlockValue.fromCoords(cc, cx, cy, cz - 1), + BlockValue.fromCoords(cc, cx + 1, cy, cz), + BlockValue.fromCoords(cc, cx, cy, cz + 1), + BlockValue.fromCoords(cc, cx, cy + 1, cz) + ); + } + else if (lv.size() == 1 + cposLocator.offset) + { + width = (int) ((NumericValue) lv.get(cposLocator.offset)).getLong(); + height = 0; + } + else if (lv.size() == 2 + cposLocator.offset) + { + width = (int) ((NumericValue) lv.get(cposLocator.offset)).getLong(); + height = (int) ((NumericValue) lv.get(cposLocator.offset + 1)).getLong(); + } + else + { + throw new InternalExpressionException("Incorrect number of arguments for 'diamond'"); + } + } + catch (ClassCastException ignored) + { + throw new InternalExpressionException("Attempted to pass a non-number to 'diamond'"); + } + if (height == 0) + { + return new LazyListValue() + { + int curradius; + int curpos; + + { + reset(); + } + + @Override + public boolean hasNext() + { + return curradius <= width; + } + + @Override + public Value next() + { + if (curradius == 0) + { + curradius = 1; + return BlockValue.fromCoords(cc, cx, cy, cz); + } + // x = 3-|i-6| + // z = |( (i-3)%12-6|-3 + Value block = BlockValue.fromCoords(cc, cx + (curradius - abs(curpos - 2 * curradius)), cy, cz - curradius + abs(abs(curpos - curradius) % (4 * curradius) - 2 * curradius)); + curpos++; + if (curpos >= curradius * 4) + { + curradius++; + curpos = 0; + } + return block; + + } + + @Override + public void reset() + { + curradius = 0; + curpos = 0; + } + + @Override + public String getString() + { + return String.format(Locale.ROOT, "diamond[(%d,%d,%d),%d,0]", cx, cy, cz, width); + } + }; + } + else + { + return new LazyListValue() + { + int curradius; + int curpos; + int curheight; + + { + reset(); + } + + @Override + public boolean hasNext() + { + return curheight <= height; + } + + @Override + public Value next() + { + if (curheight == -height || curheight == height) + { + return BlockValue.fromCoords(cc, cx, cy + curheight++, cz); + } + if (curradius == 0) + { + curradius++; + return BlockValue.fromCoords(cc, cx, cy + curheight, cz); + } + // x = 3-|i-6| + // z = |( (i-3)%12-6|-3 + + Value block = BlockValue.fromCoords(cc, cx + (curradius - abs(curpos - 2 * curradius)), cy + curheight, cz - curradius + abs(abs(curpos - curradius) % (4 * curradius) - 2 * curradius)); + curpos++; + if (curpos >= curradius * 4) + { + curradius++; + curpos = 0; + if (curradius > width - abs(width * curheight / height)) + { + curheight++; + curradius = 0; + } + } + return block; + } + + @Override + public void reset() + { + curradius = 0; + curpos = 0; + curheight = -height; + } + + @Override + public String getString() + { + return String.format(Locale.ROOT, "diamond[(%d,%d,%d),%d,%d]", cx, cy, cz, width, height); + } + }; + } + }); + } +} diff --git a/src/main/java/carpet/script/api/Entities.java b/src/main/java/carpet/script/api/Entities.java new file mode 100644 index 0000000..744f8c4 --- /dev/null +++ b/src/main/java/carpet/script/api/Entities.java @@ -0,0 +1,344 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.CarpetEventServer; +import carpet.script.CarpetScriptHost; +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.argument.FunctionArgument; +import carpet.script.argument.Vector3Argument; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.EntityValue; +import carpet.script.value.ListValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +public class Entities +{ + private static ListValue getPlayersFromWorldMatching(Context c, Predicate condition) + { + List ret = new ArrayList<>(); + for (ServerPlayer player : ((CarpetContext) c).level().players()) + { + if (condition.test(player)) + { + ret.add(new EntityValue(player)); + } + } + return ListValue.wrap(ret); + } + + public static void apply(Expression expression) + { + expression.addContextFunction("player", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + CarpetContext cc = (CarpetContext) c; + if (cc.host.user != null) + { + ServerPlayer player = cc.server().getPlayerList().getPlayerByName(cc.host.user); + return EntityValue.of(player); + } + Entity callingEntity = cc.source().getEntity(); + if (callingEntity instanceof Player) + { + return EntityValue.of(callingEntity); + } + Vec3 pos = ((CarpetContext) c).source().getPosition(); + Player closestPlayer = ((CarpetContext) c).level().getNearestPlayer(pos.x, pos.y, pos.z, -1.0, EntitySelector.ENTITY_STILL_ALIVE); + return EntityValue.of(closestPlayer); + } + String playerName = lv.get(0).getString(); + return switch (playerName) + { + case "all" -> { + List ret = new ArrayList<>(); + for (ServerPlayer player : ((CarpetContext) c).server().getPlayerList().getPlayers()) + { + ret.add(new EntityValue(player)); + } + yield ListValue.wrap(ret); + } + case "*" -> getPlayersFromWorldMatching(c, p -> true); + case "survival" -> getPlayersFromWorldMatching(c, p -> p.gameMode.isSurvival()); // includes adventure + case "creative" -> getPlayersFromWorldMatching(c, ServerPlayer::isCreative); + case "spectating" -> getPlayersFromWorldMatching(c, ServerPlayer::isSpectator); + case "!spectating" -> getPlayersFromWorldMatching(c, p -> !p.isSpectator()); + default -> { + ServerPlayer player = ((CarpetContext) c).server().getPlayerList().getPlayerByName(playerName); + yield player != null ? new EntityValue(player) : Value.NULL; + } + }; + }); + + expression.addContextFunction("spawn", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.size() < 2) + { + throw new InternalExpressionException("'spawn' function takes mob name, and position to spawn"); + } + String entityString = lv.get(0).getString(); + ResourceLocation entityId; + try + { + entityId = ResourceLocation.read(new StringReader(entityString)); + EntityType type = cc.registry(Registries.ENTITY_TYPE).getOptional(entityId).orElse(null); + if (type == null || !type.canSummon()) + { + return Value.NULL; + } + } + catch (CommandSyntaxException ignored) + { + return Value.NULL; + } + + Vector3Argument position = Vector3Argument.findIn(lv, 1); + if (position.fromBlock) + { + position.vec = position.vec.subtract(0, 0.5, 0); + } + CompoundTag tag = new CompoundTag(); + boolean hasTag = false; + if (lv.size() > position.offset) + { + Value nbt = lv.get(position.offset); + NBTSerializableValue v = (nbt instanceof final NBTSerializableValue nbtsv) + ? nbtsv + : NBTSerializableValue.parseStringOrFail(nbt.getString()); + hasTag = true; + tag = v.getCompoundTag(); + } + tag.putString("id", entityId.toString()); + Vec3 vec3d = position.vec; + + ServerLevel serverWorld = cc.level(); + Entity entity = EntityType.loadEntityRecursive(tag, serverWorld, e -> { + e.moveTo(vec3d.x, vec3d.y, vec3d.z, e.getYRot(), e.getXRot()); + return e; + }); + if (entity == null) + { + return Value.NULL; + } + if (!hasTag && entity instanceof final Mob mob) + { + mob.finalizeSpawn(serverWorld, serverWorld.getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, null); + } + if (!serverWorld.tryAddFreshEntityWithPassengers(entity)) + { + entity.discard(); + return Value.NULL; + } + return new EntityValue(entity); + }); + + expression.addContextFunction("entity_id", 1, (c, t, lv) -> + { + Value who = lv.get(0); + if (who instanceof final NumericValue numericValue) + { + return EntityValue.of(((CarpetContext) c).level().getEntity((int) numericValue.getLong())); + } + return EntityValue.of(((CarpetContext) c).level().getEntity(UUID.fromString(who.getString()))); + }); + + expression.addContextFunction("entity_list", 1, (c, t, lv) -> + { + String who = lv.get(0).getString(); + CommandSourceStack source = ((CarpetContext) c).source(); + EntityValue.EntityClassDescriptor eDesc = EntityValue.getEntityDescriptor(who, source.getServer()); + List entityList = source.getLevel().getEntities(eDesc.directType, eDesc.filteringPredicate); + return ListValue.wrap(entityList.stream().map(EntityValue::new)); + }); + + expression.addContextFunction("entity_area", -1, (c, t, lv) -> + { + if (lv.size() < 3) + { + throw new InternalExpressionException("'entity_area' requires entity type, center and range arguments"); + } + String who = lv.get(0).getString(); + CarpetContext cc = (CarpetContext) c; + Vector3Argument centerLocator = Vector3Argument.findIn(lv, 1, false, true); + + AABB centerBox; + if (centerLocator.entity != null) + { + centerBox = centerLocator.entity.getBoundingBox(); + } + else + { + Vec3 center = centerLocator.vec; + if (centerLocator.fromBlock) + { + center.add(0.5, 0.5, 0.5); + } + centerBox = new AABB(center, center); + } + Vector3Argument rangeLocator = Vector3Argument.findIn(lv, centerLocator.offset); + if (rangeLocator.fromBlock) + { + throw new InternalExpressionException("Range of 'entity_area' cannot come from a block argument"); + } + Vec3 range = rangeLocator.vec; + AABB area = centerBox.inflate(range.x, range.y, range.z); + EntityValue.EntityClassDescriptor eDesc = EntityValue.getEntityDescriptor(who, cc.server()); + List entityList = cc.level().getEntities(eDesc.directType, area, eDesc.filteringPredicate); + return ListValue.wrap(entityList.stream().map(EntityValue::new)); + }); + + expression.addContextFunction("entity_selector", -1, (c, t, lv) -> + { + String selector = lv.get(0).getString(); + List retlist = new ArrayList<>(); + for (Entity e : EntityValue.getEntitiesFromSelector(((CarpetContext) c).source(), selector)) + { + retlist.add(new EntityValue(e)); + } + return ListValue.wrap(retlist); + }); + + expression.addContextFunction("query", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'query' takes entity as a first argument, and queried feature as a second"); + } + Value v = lv.get(0); + if (!(v instanceof final EntityValue ev)) + { + throw new InternalExpressionException("First argument to query should be an entity"); + } + String what = lv.get(1).getString().toLowerCase(Locale.ROOT); + if (what.equals("tags")) + { + c.host.issueDeprecation("'tags' for entity querying"); + } + return switch (lv.size()) + { + case 2 -> ev.get(what, null); + case 3 -> ev.get(what, lv.get(2)); + default -> ev.get(what, ListValue.wrap(lv.subList(2, lv.size()))); + }; + }); + + // or update + expression.addContextFunction("modify", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'modify' takes entity as a first argument, and queried feature as a second"); + } + Value v = lv.get(0); + if (!(v instanceof final EntityValue ev)) + { + throw new InternalExpressionException("First argument to modify should be an entity"); + } + String what = lv.get(1).getString(); + switch (lv.size()) + { + case 2 -> ev.set(what, null); + case 3 -> ev.set(what, lv.get(2)); + default -> ev.set(what, ListValue.wrap(lv.subList(2, lv.size()))); + } + return v; + }); + + expression.addContextFunction("entity_types", -1, (c, t, lv) -> + { + if (lv.size() > 1) + { + throw new InternalExpressionException("'entity_types' requires one or no arguments"); + } + String desc = (lv.size() == 1) ? lv.get(0).getString() : "*"; + return EntityValue.getEntityDescriptor(desc, ((CarpetContext) c).server()).listValue(((CarpetContext) c).registryAccess()); + }); + + expression.addContextFunction("entity_load_handler", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'entity_load_handler' required the entity type, and a function to call"); + } + Value entityValue = lv.get(0); + List descriptors = (entityValue instanceof final ListValue list) + ? list.getItems().stream().map(Value::getString).toList() + : Collections.singletonList(entityValue.getString()); + Set> types = new HashSet<>(); + descriptors.forEach(s -> types.addAll(EntityValue.getEntityDescriptor(s, ((CarpetContext) c).server()).types)); + FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 1, true, false); + CarpetEventServer events = ((CarpetScriptHost) c.host).scriptServer().events; + if (funArg.function == null) + { + types.forEach(et -> events.removeBuiltInEvent(CarpetEventServer.Event.getEntityLoadEventName(et), (CarpetScriptHost) c.host)); + types.forEach(et -> events.removeBuiltInEvent(CarpetEventServer.Event.getEntityHandlerEventName(et), (CarpetScriptHost) c.host)); + } + else + { + ///compat + int numberOfArguments = funArg.function.getArguments().size() - funArg.args.size(); + if (numberOfArguments == 1) + { + c.host.issueDeprecation("entity_load_handler() with single argument callback"); + types.forEach(et -> events.addBuiltInEvent(CarpetEventServer.Event.getEntityLoadEventName(et), c.host, funArg.function, funArg.args)); + } + else + { + types.forEach(et -> events.addBuiltInEvent(CarpetEventServer.Event.getEntityHandlerEventName(et), c.host, funArg.function, funArg.args)); + } + } + return new NumericValue(types.size()); + }); + + // or update + expression.addContextFunction("entity_event", -1, (c, t, lv) -> + { + if (lv.size() < 3) + { + throw new InternalExpressionException("'entity_event' requires at least 3 arguments, entity, event to be handled, and function name, with optional arguments"); + } + Value v = lv.get(0); + if (!(v instanceof final EntityValue ev)) + { + throw new InternalExpressionException("First argument to entity_event should be an entity"); + } + String what = lv.get(1).getString(); + + FunctionArgument funArg = FunctionArgument.findIn(c, expression.module, lv, 2, true, false); + + ev.setEvent((CarpetContext) c, what, funArg.function, funArg.args); + + return Value.NULL; + }); + } +} diff --git a/src/main/java/carpet/script/api/Inventories.java b/src/main/java/carpet/script/api/Inventories.java new file mode 100644 index 0000000..11bab7e --- /dev/null +++ b/src/main/java/carpet/script/api/Inventories.java @@ -0,0 +1,517 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.Expression; +import carpet.script.argument.FunctionArgument; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.external.Vanilla; +import carpet.script.utils.InputValidator; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.ListValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.ScreenValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.TagKey; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.CustomRecipe; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.item.crafting.SingleItemRecipe; +import net.minecraft.world.phys.Vec3; + +public class Inventories +{ + public static void apply(Expression expression) + { + expression.addContextFunction("stack_limit", 1, (c, t, lv) -> + new NumericValue(NBTSerializableValue.parseItem(lv.get(0).getString(), ((CarpetContext) c).registryAccess()).getMaxStackSize())); + + expression.addContextFunction("item_category", -1, (c, t, lv) -> { + c.host.issueDeprecation("item_category in 1.19.3+"); + return Value.NULL; + }); + + expression.addContextFunction("item_list", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + Registry items = cc.registry(Registries.ITEM); + if (lv.isEmpty()) + { + return ListValue.wrap(items.holders().map(itemReference -> ValueConversions.of(itemReference.key().location()))); + } + String tag = lv.get(0).getString(); + Optional> itemTag = items.getTag(TagKey.create(Registries.ITEM, InputValidator.identifierOf(tag))); + return itemTag.isEmpty() ? Value.NULL : ListValue.wrap(itemTag.get().stream().map(b -> items.getKey(b.value())).filter(Objects::nonNull).map(ValueConversions::of)); + }); + + expression.addContextFunction("item_tags", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + + Registry blocks = cc.registry(Registries.ITEM); + if (lv.isEmpty()) + { + return ListValue.wrap(blocks.getTagNames().map(ValueConversions::of)); + } + Item item = NBTSerializableValue.parseItem(lv.get(0).getString(), cc.registryAccess()).getItem(); + if (lv.size() == 1) + { + return ListValue.wrap(blocks.getTags().filter(e -> e.getSecond().stream().anyMatch(h -> (h.value() == item))).map(e -> ValueConversions.of(e.getFirst()))); + } + String tag = lv.get(1).getString(); + Optional> tagSet = blocks.getTag(TagKey.create(Registries.ITEM, InputValidator.identifierOf(tag))); + return tagSet.isEmpty() ? Value.NULL : BooleanValue.of(tagSet.get().stream().anyMatch(h -> h.value() == item)); + }); + + expression.addContextFunction("recipe_data", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.size() < 1) + { + throw new InternalExpressionException("'recipe_data' requires at least one argument"); + } + String recipeName = lv.get(0).getString(); + RecipeType type = RecipeType.CRAFTING; + if (lv.size() > 1) + { + String recipeType = lv.get(1).getString(); + type = cc.registry(Registries.RECIPE_TYPE).get(InputValidator.identifierOf(recipeType)); + if (type == null) + { + throw new InternalExpressionException("Unknown recipe type: " + recipeType); + } + } + List> recipes = Vanilla.RecipeManager_getAllMatching(cc.server().getRecipeManager(), type, InputValidator.identifierOf(recipeName), cc.registryAccess()); + if (recipes.isEmpty()) + { + return Value.NULL; + } + List recipesOutput = new ArrayList<>(); + RegistryAccess regs = cc.registryAccess(); + for (Recipe recipe : recipes) + { + ItemStack result = recipe.getResultItem(regs); + List ingredientValue = new ArrayList<>(); + recipe.getIngredients().forEach(ingredient -> { + // I am flattening ingredient lists per slot. + // consider recipe_data('wooden_sword','crafting') and ('iron_nugget', 'blasting') and notice difference + // in depths of lists. + List> stacks = Vanilla.Ingredient_getRecipeStacks(ingredient); + if (stacks.isEmpty()) + { + ingredientValue.add(Value.NULL); + } + else + { + List alternatives = new ArrayList<>(); + stacks.forEach(col -> col.stream().map(is -> ValueConversions.of(is, regs)).forEach(alternatives::add)); + ingredientValue.add(ListValue.wrap(alternatives)); + } + }); + Value recipeSpec; + if (recipe instanceof ShapedRecipe shapedRecipe) + { + recipeSpec = ListValue.of( + new StringValue("shaped"), + new NumericValue(shapedRecipe.getWidth()), + new NumericValue(shapedRecipe.getHeight()) + ); + } + else if (recipe instanceof ShapelessRecipe) + { + recipeSpec = ListValue.of(new StringValue("shapeless")); + } + else if (recipe instanceof AbstractCookingRecipe abstractCookingRecipe) + { + recipeSpec = ListValue.of( + new StringValue("smelting"), + new NumericValue(abstractCookingRecipe.getCookingTime()), + new NumericValue(abstractCookingRecipe.getExperience()) + ); + } + else if (recipe instanceof SingleItemRecipe) + { + recipeSpec = ListValue.of(new StringValue("cutting")); + } + else if (recipe instanceof CustomRecipe) + { + recipeSpec = ListValue.of(new StringValue("special")); + } + else + { + recipeSpec = ListValue.of(new StringValue("custom")); + } + + recipesOutput.add(ListValue.of(ValueConversions.of(result, regs), ListValue.wrap(ingredientValue), recipeSpec)); + } + return ListValue.wrap(recipesOutput); + }); + + expression.addContextFunction("crafting_remaining_item", 1, (c, t, v) -> + { + String itemStr = v.get(0).getString(); + ResourceLocation id = InputValidator.identifierOf(itemStr); + Registry registry = ((CarpetContext) c).registry(Registries.ITEM); + Item item = registry.getOptional(id).orElseThrow(() -> new ThrowStatement(itemStr, Throwables.UNKNOWN_ITEM)); + Item reminder = item.getCraftingRemainingItem(); + return reminder == null ? Value.NULL : NBTSerializableValue.nameFromRegistryId(registry.getKey(reminder)); + }); + + expression.addContextFunction("inventory_size", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + return inventoryLocator == null ? Value.NULL : new NumericValue(inventoryLocator.inventory().getContainerSize()); + }); + + expression.addContextFunction("inventory_has_items", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + return inventoryLocator == null ? Value.NULL : BooleanValue.of(!inventoryLocator.inventory().isEmpty()); + }); + + //inventory_get(, ) -> item_triple + expression.addContextFunction("inventory_get", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + if (inventoryLocator == null) + { + return Value.NULL; + } + RegistryAccess regs = cc.registryAccess(); + if (lv.size() == inventoryLocator.offset()) + { + List fullInventory = new ArrayList<>(); + for (int i = 0, maxi = inventoryLocator.inventory().getContainerSize(); i < maxi; i++) + { + fullInventory.add(ValueConversions.of(inventoryLocator.inventory().getItem(i), regs)); + } + return ListValue.wrap(fullInventory); + } + int slot = (int) NumericValue.asNumber(lv.get(inventoryLocator.offset())).getLong(); + slot = NBTSerializableValue.validateSlot(slot, inventoryLocator.inventory()); + return slot == inventoryLocator.inventory().getContainerSize() + ? Value.NULL + : ValueConversions.of(inventoryLocator.inventory().getItem(slot), regs); + }); + + //inventory_set(, , , , ) + expression.addContextFunction("inventory_set", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + if (inventoryLocator == null) + { + return Value.NULL; + } + if (lv.size() < inventoryLocator.offset() + 2) + { + throw new InternalExpressionException("'inventory_set' requires at least slot number and new stack size, and optional new item"); + } + int slot = (int) NumericValue.asNumber(lv.get(inventoryLocator.offset())).getLong(); + slot = NBTSerializableValue.validateSlot(slot, inventoryLocator.inventory()); + if (slot == inventoryLocator.inventory().getContainerSize()) + { + return Value.NULL; + } + OptionalInt count = OptionalInt.empty(); + + Value countVal = lv.get(inventoryLocator.offset() + 1); + if (!countVal.isNull()) + { + count = OptionalInt.of((int) NumericValue.asNumber(countVal).getLong()); + } + RegistryAccess regs = cc.registryAccess(); + if (count.isPresent() && count.getAsInt() == 0) + { + // clear slot + ItemStack removedStack = inventoryLocator.inventory().removeItemNoUpdate(slot); + syncPlayerInventory(inventoryLocator); + return ValueConversions.of(removedStack, regs); + } + if (lv.size() < inventoryLocator.offset() + 3) + { + ItemStack previousStack = inventoryLocator.inventory().getItem(slot); + ItemStack newStack = previousStack.copy(); + count.ifPresent(newStack::setCount); + inventoryLocator.inventory().setItem(slot, newStack); + syncPlayerInventory(inventoryLocator); + return ValueConversions.of(previousStack, regs); + } + CompoundTag nbt = null; // skipping one argument, item name + if (lv.size() > inventoryLocator.offset() + 3) + { + Value nbtValue = lv.get(inventoryLocator.offset() + 3); + if (nbtValue instanceof NBTSerializableValue nbtsv) + { + nbt = nbtsv.getCompoundTag(); + } + else if (!nbtValue.isNull()) + { + nbt = new NBTSerializableValue(nbtValue.getString()).getCompoundTag(); + } + } + ItemStack newitem = NBTSerializableValue.parseItem(lv.get(inventoryLocator.offset() + 2).getString(), nbt, cc.registryAccess()); + count.ifPresent(newitem::setCount); + ItemStack previousStack = inventoryLocator.inventory().getItem(slot); + inventoryLocator.inventory().setItem(slot, newitem); + syncPlayerInventory(inventoryLocator); + + return ValueConversions.of(previousStack, regs); + }); + + //inventory_find(, or null (first empty slot), ) -> or null + expression.addContextFunction("inventory_find", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + if (inventoryLocator == null) + { + return Value.NULL; + } + ItemStack itemArg = null; + if (lv.size() > inventoryLocator.offset()) + { + Value secondArg = lv.get(inventoryLocator.offset()); + if (!secondArg.isNull()) + { + itemArg = NBTSerializableValue.parseItem(secondArg.getString(), cc.registryAccess()); + } + } + int startIndex = 0; + if (lv.size() > inventoryLocator.offset() + 1) + { + startIndex = (int) NumericValue.asNumber(lv.get(inventoryLocator.offset() + 1)).getLong(); + } + startIndex = NBTSerializableValue.validateSlot(startIndex, inventoryLocator.inventory()); + for (int i = startIndex, maxi = inventoryLocator.inventory().getContainerSize(); i < maxi; i++) + { + ItemStack stack = inventoryLocator.inventory().getItem(i); + if ((itemArg == null && stack.isEmpty()) || (itemArg != null && itemArg.getItem().equals(stack.getItem()))) + { + return new NumericValue(i); + } + } + return Value.NULL; + }); + + //inventory_remove(, , ) -> bool + expression.addContextFunction("inventory_remove", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + if (inventoryLocator == null) + { + return Value.NULL; + } + if (lv.size() <= inventoryLocator.offset()) + { + throw new InternalExpressionException("'inventory_remove' requires at least an item to be removed"); + } + ItemStack searchItem = NBTSerializableValue.parseItem(lv.get(inventoryLocator.offset()).getString(), cc.registryAccess()); + int amount = 1; + if (lv.size() > inventoryLocator.offset() + 1) + { + amount = (int) NumericValue.asNumber(lv.get(inventoryLocator.offset() + 1)).getLong(); + } + // not enough + if (((amount == 1) && (!inventoryLocator.inventory().hasAnyOf(Set.of(searchItem.getItem())))) + || (inventoryLocator.inventory().countItem(searchItem.getItem()) < amount)) + { + return Value.FALSE; + } + for (int i = 0, maxi = inventoryLocator.inventory().getContainerSize(); i < maxi; i++) + { + ItemStack stack = inventoryLocator.inventory().getItem(i); + if (stack.isEmpty() || !stack.getItem().equals(searchItem.getItem())) + { + continue; + } + int left = stack.getCount() - amount; + if (left > 0) + { + stack.setCount(left); + inventoryLocator.inventory().setItem(i, stack); + syncPlayerInventory(inventoryLocator); + return Value.TRUE; + } + inventoryLocator.inventory().removeItemNoUpdate(i); + syncPlayerInventory(inventoryLocator); + amount -= stack.getCount(); + } + if (amount > 0) + { + throw new InternalExpressionException("Something bad happened - cannot pull all items from inventory"); + } + return Value.TRUE; + }); + + //inventory_drop(, , ) -> entity_item (and sets slot) or null if cannot + expression.addContextFunction("drop_item", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + NBTSerializableValue.InventoryLocator inventoryLocator = NBTSerializableValue.locateInventory(cc, lv, 0); + if (inventoryLocator == null) + { + return Value.NULL; + } + if (lv.size() == inventoryLocator.offset()) + { + throw new InternalExpressionException("Slot number is required for inventory_drop"); + } + int slot = (int) NumericValue.asNumber(lv.get(inventoryLocator.offset())).getLong(); + slot = NBTSerializableValue.validateSlot(slot, inventoryLocator.inventory()); + if (slot == inventoryLocator.inventory().getContainerSize()) + { + return Value.NULL; + } + int amount = 0; + if (lv.size() > inventoryLocator.offset() + 1) + { + amount = (int) NumericValue.asNumber(lv.get(inventoryLocator.offset() + 1)).getLong(); + } + if (amount < 0) + { + throw new InternalExpressionException("Cannot throw negative number of items"); + } + ItemStack stack = inventoryLocator.inventory().getItem(slot); + if (stack == null || stack.isEmpty()) + { + return Value.ZERO; + } + if (amount == 0) + { + amount = stack.getCount(); + } + ItemStack droppedStack = inventoryLocator.inventory().removeItem(slot, amount); + if (droppedStack.isEmpty()) + { + return Value.ZERO; + } + Object owner = inventoryLocator.owner(); + ItemEntity item; + if (owner instanceof Player player) + { + item = player.drop(droppedStack, false, true); + if (item == null) + { + return Value.ZERO; + } + } + else if (owner instanceof LivingEntity livingEntity) + { + // stolen from LookTargetUtil.give((VillagerEntity)owner, droppedStack, (LivingEntity) owner); + double dropY = livingEntity.getY() - 0.30000001192092896D + livingEntity.getEyeHeight(); + item = new ItemEntity(livingEntity.level(), livingEntity.getX(), dropY, livingEntity.getZ(), droppedStack); + Vec3 vec3d = livingEntity.getViewVector(1.0F).normalize().scale(0.3);// new Vec3d(0, 0.3, 0); + item.setDeltaMovement(vec3d); + item.setDefaultPickUpDelay(); + cc.level().addFreshEntity(item); + } + else + { + Vec3 point = Vec3.atCenterOf(inventoryLocator.position()); //pos+0.5v + item = new ItemEntity(cc.level(), point.x, point.y, point.z, droppedStack); + item.setDefaultPickUpDelay(); + cc.level().addFreshEntity(item); + } + return new NumericValue(item.getItem().getCount()); + }); + + expression.addContextFunction("create_screen", -1, (c, t, lv) -> + { + if (lv.size() < 3) + { + throw new InternalExpressionException("'create_screen' requires at least three arguments"); + } + Value playerValue = lv.get(0); + ServerPlayer player = EntityValue.getPlayerByValue(((CarpetContext) c).server(), playerValue); + if (player == null) + { + throw new InternalExpressionException("'create_screen' requires a valid online player as the first argument."); + } + String type = lv.get(1).getString(); + Component name = FormattedTextValue.getTextByValue(lv.get(2)); + FunctionValue function = null; + if (lv.size() > 3) + { + function = FunctionArgument.findIn(c, expression.module, lv, 3, true, false).function; + } + + return new ScreenValue(player, type, name, function, c); + }); + + expression.addContextFunction("close_screen", 1, (c, t, lv) -> + { + Value value = lv.get(0); + if (!(value instanceof ScreenValue screenValue)) + { + throw new InternalExpressionException("'close_screen' requires a screen value as the first argument."); + } + if (!screenValue.isOpen()) + { + return Value.FALSE; + } + screenValue.close(); + return Value.TRUE; + }); + + expression.addContextFunction("screen_property", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'screen_property' requires at least a screen and a property name"); + } + if (!(lv.get(0) instanceof ScreenValue screenValue)) + { + throw new InternalExpressionException("'screen_property' requires a screen value as the first argument"); + } + String propertyName = lv.get(1).getString(); + return lv.size() >= 3 + ? screenValue.modifyProperty(propertyName, lv.subList(2, lv.size())) + : screenValue.queryProperty(propertyName); + }); + } + + private static void syncPlayerInventory(NBTSerializableValue.InventoryLocator inventory) + { + if (inventory.owner() instanceof ServerPlayer player && !inventory.isEnder() && !(inventory.inventory() instanceof ScreenValue.ScreenHandlerInventory)) + { + player.containerMenu.broadcastChanges(); + } + } +} diff --git a/src/main/java/carpet/script/api/Monitoring.java b/src/main/java/carpet/script/api/Monitoring.java new file mode 100644 index 0000000..5f232af --- /dev/null +++ b/src/main/java/carpet/script/api/Monitoring.java @@ -0,0 +1,89 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.Expression; +import carpet.script.exception.InternalExpressionException; +import carpet.script.external.Vanilla; +import carpet.script.utils.SystemInfo; +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 it.unimi.dsi.fastutil.objects.Object2IntMap; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.NaturalSpawner; + +public class Monitoring +{ + private static final Map MOB_CATEGORY_MAP = Arrays.stream(MobCategory.values()).collect(Collectors.toMap(MobCategory::getName, Function.identity())); + + public static void apply(Expression expression) + { + expression.addContextFunction("system_info", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + return SystemInfo.getAll(); + } + if (lv.size() == 1) + { + String what = lv.get(0).getString(); + Value res = SystemInfo.get(what, (CarpetContext) c); + if (res == null) + { + throw new InternalExpressionException("Unknown option for 'system_info': " + what); + } + return res; + } + throw new InternalExpressionException("'system_info' requires one or no parameters"); + }); + // game processed snooper functions + expression.addContextFunction("get_mob_counts", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerLevel world = cc.level(); + NaturalSpawner.SpawnState info = world.getChunkSource().getLastSpawnState(); + if (info == null) + { + return Value.NULL; + } + Object2IntMap mobcounts = info.getMobCategoryCounts(); + int chunks = info.getSpawnableChunkCount(); + if (lv.isEmpty()) + { + Map retDict = new HashMap<>(); + for (MobCategory category : mobcounts.keySet()) + { + int currentCap = category.getMaxInstancesPerChunk() * chunks / Vanilla.NaturalSpawner_MAGIC_NUMBER(); + retDict.put( + new StringValue(category.getSerializedName().toLowerCase(Locale.ROOT)), + ListValue.of( + new NumericValue(mobcounts.getInt(category)), + new NumericValue(currentCap)) + ); + } + return MapValue.wrap(retDict); + } + String catString = lv.get(0).getString(); + MobCategory cat = MOB_CATEGORY_MAP.get(catString.toLowerCase(Locale.ROOT)); + if (cat == null) + { + throw new InternalExpressionException("Unreconized mob category: " + catString); + } + return ListValue.of( + new NumericValue(mobcounts.getInt(cat)), + new NumericValue((long) cat.getMaxInstancesPerChunk() * chunks / Vanilla.NaturalSpawner_MAGIC_NUMBER()) + ); + }); + } +} diff --git a/src/main/java/carpet/script/api/Scoreboards.java b/src/main/java/carpet/script/api/Scoreboards.java new file mode 100644 index 0000000..cd282d7 --- /dev/null +++ b/src/main/java/carpet/script/api/Scoreboards.java @@ -0,0 +1,701 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.Expression; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.external.Vanilla; +import carpet.script.utils.InputValidator; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.Value; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.ServerScoreboard; +import net.minecraft.server.bossevents.CustomBossEvent; +import net.minecraft.server.bossevents.CustomBossEvents; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.BossEvent; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.ScoreAccess; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.Team; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; + +public class Scoreboards +{ + private static ScoreHolder getScoreboardKeyFromValue(Value keyValue) + { + return keyValue instanceof EntityValue ev + ? ev.getEntity() + : ScoreHolder.forNameOnly(keyValue.getString()); + } + + public static void apply(Expression expression) + { + // scoreboard(player,'objective') + // scoreboard(player, objective, newValue) + expression.addContextFunction("scoreboard", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + Scoreboard scoreboard = cc.server().getScoreboard(); + if (lv.isEmpty()) + { + return ListValue.wrap(scoreboard.getObjectiveNames().stream().map(StringValue::new)); + } + String objectiveName = lv.get(0).getString(); + Objective objective = scoreboard.getObjective(objectiveName); + if (objective == null) + { + return Value.NULL; + } + if (lv.size() == 1) + { + return ListValue.wrap(scoreboard.listPlayerScores(objective).stream().map(s -> new StringValue(s.owner()))); + } + ScoreHolder key = getScoreboardKeyFromValue(lv.get(1)); + if (lv.size() == 2) + { + return scoreboard.getPlayerScoreInfo(key, objective) == null + ? Value.NULL + : NumericValue.of(scoreboard.getOrCreatePlayerScore(key, objective).get()); + } + + Value value = lv.get(2); + if (value.isNull()) + { + int score = scoreboard.getOrCreatePlayerScore(key, objective).get(); + scoreboard.resetSinglePlayerScore(key, objective); + return NumericValue.of(score); + } + if (value instanceof NumericValue) + { + ScoreAccess score = scoreboard.getOrCreatePlayerScore(key, objective); + int previous = score.get(); + score.set(NumericValue.asNumber(value).getInt()); + return NumericValue.of(previous); + } + throw new InternalExpressionException("'scoreboard' requires a number or null as the third parameter"); + }); + + expression.addContextFunction("scoreboard_remove", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'scoreboard_remove' requires at least one parameter"); + } + CarpetContext cc = (CarpetContext) c; + Scoreboard scoreboard = cc.server().getScoreboard(); + String objectiveName = lv.get(0).getString(); + Objective objective = scoreboard.getObjective(objectiveName); + if (objective == null) + { + return Value.FALSE; + } + if (lv.size() == 1) + { + scoreboard.removeObjective(objective); + return Value.TRUE; + } + ScoreHolder key = getScoreboardKeyFromValue(lv.get(1)); + if (scoreboard.getPlayerScoreInfo(key, objective) == null) + { + return Value.NULL; + } + ScoreAccess scoreboardPlayerScore = scoreboard.getOrCreatePlayerScore(key, objective); + Value previous = new NumericValue(scoreboardPlayerScore.get()); + scoreboard.resetSinglePlayerScore(key, objective); + return previous; + }); + + // objective_add('lvl','level') + // objective_add('counter') + + expression.addContextFunction("scoreboard_add", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + Scoreboard scoreboard = cc.server().getScoreboard(); + if (lv.isEmpty() || lv.size() > 2) + { + throw new InternalExpressionException("'scoreboard_add' should have one or two parameters"); + } + String objectiveName = lv.get(0).getString(); + ObjectiveCriteria criterion; + if (lv.size() == 1) + { + criterion = ObjectiveCriteria.DUMMY; + } + else + { + String critetionName = lv.get(1).getString(); + criterion = ObjectiveCriteria.byName(critetionName).orElse(null); + if (criterion == null) + { + throw new ThrowStatement(critetionName, Throwables.UNKNOWN_CRITERION); + } + } + + Objective objective = scoreboard.getObjective(objectiveName); + if (objective != null) + { + c.host.issueDeprecation("reading or modifying an objective's criterion with scoreboard_add"); + if (lv.size() == 1) + { + return StringValue.of(objective.getCriteria().getName()); + } + if (objective.getCriteria().equals(criterion) || lv.size() == 1) + { + return Value.NULL; + } + Vanilla.Scoreboard_getObjectivesByCriterion(scoreboard).get(objective.getCriteria()).remove(objective); + Vanilla.Objective_setCriterion(objective, criterion); + (Vanilla.Scoreboard_getObjectivesByCriterion(scoreboard).computeIfAbsent(criterion, cr -> Lists.newArrayList())).add(objective); + scoreboard.onObjectiveAdded(objective); + return Value.FALSE; + } + scoreboard.addObjective(objectiveName, criterion, Component.literal(objectiveName), criterion.getDefaultRenderType(), false, null); + return Value.TRUE; + }); + + expression.addContextFunction("scoreboard_property", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'scoreboard_property' requires at least two parameters"); + } + CarpetContext cc = (CarpetContext) c; + Scoreboard scoreboard = cc.server().getScoreboard(); + Objective objective = scoreboard.getObjective(lv.get(0).getString()); + if (objective == null) + { + return Value.NULL; + } + + boolean modify = lv.size() > 2; + Value setValue = null; + if (modify) + { + setValue = lv.get(2); + } + String property = lv.get(1).getString(); + switch (property) + { + case "criterion" -> { + if (modify) + { + ObjectiveCriteria criterion = ObjectiveCriteria.byName(setValue.getString()).orElse(null); + if (criterion == null) + { + throw new InternalExpressionException("Unknown scoreboard criterion: " + setValue.getString()); + } + if (objective.getCriteria().equals(criterion) || lv.size() == 1) + { + return Value.FALSE; + } + Vanilla.Scoreboard_getObjectivesByCriterion(scoreboard).get(objective.getCriteria()).remove(objective); + Vanilla.Objective_setCriterion(objective, criterion); + (Vanilla.Scoreboard_getObjectivesByCriterion(scoreboard).computeIfAbsent(criterion, cr -> Lists.newArrayList())).add(objective); + scoreboard.onObjectiveAdded(objective); + return Value.TRUE; + } + return StringValue.of(objective.getCriteria().getName()); + } + case "display_name" -> { + if (modify) + { + Component text = FormattedTextValue.getTextByValue(setValue); + objective.setDisplayName(text); + return Value.TRUE; + } + return new FormattedTextValue(objective.getDisplayName()); + } + case "display_slot" -> { + if (modify) + { + DisplaySlot slot = DisplaySlot.CODEC.byName(setValue.getString()); + if (slot == null) + { + throw new InternalExpressionException("Unknown scoreboard display slot: " + setValue.getString()); + } + if (objective.equals(scoreboard.getDisplayObjective(slot))) + { + return Value.FALSE; + } + scoreboard.setDisplayObjective(slot, objective); + return Value.TRUE; + } + List slots = new ArrayList<>(); + for (DisplaySlot slot : DisplaySlot.values()) + { + if (scoreboard.getDisplayObjective(slot) == objective) + { + slots.add(StringValue.of(slot.getSerializedName())); + } + } + return ListValue.wrap(slots); + } + case "render_type" -> { + if (modify) + { + ObjectiveCriteria.RenderType renderType = ObjectiveCriteria.RenderType.byId(setValue.getString().toLowerCase()); + if (objective.getRenderType().equals(renderType)) + { + return Value.FALSE; + } + objective.setRenderType(renderType); + return Value.TRUE; + } + return StringValue.of(objective.getRenderType().getId()); + } + default -> throw new InternalExpressionException("scoreboard property '" + property + "' is not a valid property"); + } + }); + + expression.addContextFunction("scoreboard_display", 2, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + Scoreboard scoreboard = cc.server().getScoreboard(); + String location = lv.get(0).getString(); + DisplaySlot slot = DisplaySlot.CODEC.byName(location); + if (slot == null) + { + throw new InternalExpressionException("Invalid objective slot: " + location); + } + Value target = lv.get(1); + if (target.isNull()) + { + scoreboard.setDisplayObjective(slot, null); + return StringValue.of(slot.getSerializedName()); + } + String objectiveString = target.getString(); + Objective objective = scoreboard.getObjective(objectiveString); + if (objective == null) + { + return Value.NULL; + } + scoreboard.setDisplayObjective(slot, objective); + return StringValue.of(slot.getSerializedName()); + }); + + expression.addContextFunction("team_list", -1, (c, t, lv) -> + { + if (lv.size() > 1) + { + throw new InternalExpressionException("'team_list' requires zero or one parameters"); + } + CarpetContext cc = (CarpetContext) c; + ServerScoreboard scoreboard = cc.server().getScoreboard(); + if (lv.isEmpty()) + { + return ListValue.wrap(scoreboard.getTeamNames().stream().map(StringValue::of)); + } + if (lv.size() != 1) + { + return Value.NULL; + } + PlayerTeam team = scoreboard.getPlayerTeam(lv.get(0).getString()); + return team == null ? Value.NULL : ListValue.wrap(team.getPlayers().stream().map(StringValue::of)); + }); + + + expression.addContextFunction("team_add", -1, (c, t, lv) -> + { + if (!(lv.size() < 3 && !lv.isEmpty())) + { + throw new InternalExpressionException("'team_add' requires one or two parameters"); + } + + CarpetContext cc = (CarpetContext) c; + ServerScoreboard scoreboard = cc.server().getScoreboard(); + String teamName = lv.get(0).getString(); + + if (lv.size() == 1) + { + if (scoreboard.getPlayerTeam(teamName) != null) + { + return Value.NULL; + } + scoreboard.addPlayerTeam(teamName); + return new StringValue(teamName); + } + if (lv.size() != 2) + { + return Value.NULL; + } + Value playerVal = lv.get(1); + String player = EntityValue.getPlayerNameByValue(playerVal); + if (player == null) + { + return Value.NULL; + } + PlayerTeam team = scoreboard.getPlayerTeam(teamName); + if (team == null) + { + return Value.NULL; + } + if (team.isAlliedTo(scoreboard.getPlayersTeam(player))) + { + return Value.FALSE; + } + scoreboard.addPlayerToTeam(player, team); + return Value.TRUE; + }); + + expression.addContextFunction("team_remove", 1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerScoreboard scoreboard = cc.server().getScoreboard(); + Value teamVal = lv.get(0); + PlayerTeam team = scoreboard.getPlayerTeam(teamVal.getString()); + if (team == null) + { + return Value.NULL; + } + scoreboard.removePlayerTeam(team); + return Value.TRUE; + }); + + + expression.addContextFunction("team_leave", 1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerScoreboard scoreboard = cc.server().getScoreboard(); + Value playerVal = lv.get(0); + String player = EntityValue.getPlayerNameByValue(playerVal); + return player == null ? Value.NULL : BooleanValue.of(scoreboard.removePlayerFromTeam(player)); + }); + + expression.addContextFunction("team_property", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerScoreboard scoreboard = cc.server().getScoreboard(); + if (lv.size() < 2 || lv.size() > 3) + { + throw new InternalExpressionException("'team_property' requires two or three arguments"); + } + Value teamVal = lv.get(0); + Value propertyVal = lv.get(1); + + Value settingVal = null; + boolean modifying = false; + if (lv.size() == 3) + { + modifying = true; + settingVal = lv.get(2); + } + + PlayerTeam team = scoreboard.getPlayerTeam(teamVal.getString()); + if (team == null) + { + return Value.NULL; + } + + if (!(propertyVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string as the second argument"); + } + + switch (propertyVal.getString()) + { + case "collisionRule" -> { + if (!modifying) + { + return new StringValue(team.getCollisionRule().name); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string as the third argument for the property " + propertyVal.getString()); + } + Team.CollisionRule collisionRule = Team.CollisionRule.byName(settingVal.getString()); + if (collisionRule == null) + { + throw new InternalExpressionException("Unknown value for property " + propertyVal.getString() + ": " + settingVal.getString()); + } + team.setCollisionRule(collisionRule); + } + case "color" -> { + if (!modifying) + { + return new StringValue(team.getColor().getName()); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string as the third argument for the property " + propertyVal.getString()); + } + ChatFormatting color = ChatFormatting.getByName(settingVal.getString().toUpperCase()); + if (color == null || !color.isColor()) + { + throw new InternalExpressionException("Unknown value for property " + propertyVal.getString() + ": " + settingVal.getString()); + } + team.setColor(color); + } + case "deathMessageVisibility" -> { + if (!modifying) + { + return new StringValue(team.getDeathMessageVisibility().name); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string as the third argument for the property " + propertyVal.getString()); + } + Team.Visibility deathMessageVisibility = Team.Visibility.byName(settingVal.getString()); + if (deathMessageVisibility == null) + { + throw new InternalExpressionException("Unknown value for property " + propertyVal.getString() + ": " + settingVal.getString()); + } + team.setDeathMessageVisibility(deathMessageVisibility); + } + case "displayName" -> { + if (!modifying) + { + return new FormattedTextValue(team.getDisplayName()); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string or formatted text as the third argument for the property " + propertyVal.getString()); + } + team.setDisplayName(FormattedTextValue.getTextByValue(settingVal)); + } + case "friendlyFire" -> { + if (!modifying) + { + return BooleanValue.of(team.isAllowFriendlyFire()); + } + if (!(settingVal instanceof NumericValue)) + { + throw new InternalExpressionException("'team_property' requires a boolean as the third argument for the property " + propertyVal.getString()); + } + team.setAllowFriendlyFire(settingVal.getBoolean()); + } + case "nametagVisibility" -> { + if (!modifying) + { + return new StringValue(team.getNameTagVisibility().name); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string as the third argument for the property " + propertyVal.getString()); + } + Team.Visibility nametagVisibility = Team.Visibility.byName(settingVal.getString()); + if (nametagVisibility == null) + { + throw new InternalExpressionException("Unknown value for property " + propertyVal.getString() + ": " + settingVal.getString()); + } + team.setNameTagVisibility(nametagVisibility); + } + case "prefix" -> { + if (!modifying) + { + return new FormattedTextValue(team.getPlayerPrefix()); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property ' requires a string or formatted text as the third argument for the property " + propertyVal.getString()); + } + team.setPlayerPrefix(FormattedTextValue.getTextByValue(settingVal)); + } + case "seeFriendlyInvisibles" -> { + if (!modifying) + { + return BooleanValue.of(team.canSeeFriendlyInvisibles()); + } + if (!(settingVal instanceof NumericValue)) + { + throw new InternalExpressionException("'team_property' requires a boolean as the third argument for the property " + propertyVal.getString()); + } + team.setSeeFriendlyInvisibles(settingVal.getBoolean()); + } + case "suffix" -> { + if (!modifying) + { + return new FormattedTextValue(team.getPlayerSuffix()); + } + if (!(settingVal instanceof StringValue)) + { + throw new InternalExpressionException("'team_property' requires a string or formatted text as the third argument for the property " + propertyVal.getString()); + } + team.setPlayerSuffix(FormattedTextValue.getTextByValue(settingVal)); + } + default -> throw new InternalExpressionException("team property '" + propertyVal.getString() + "' is not a valid property"); + } + return Value.TRUE; + }); + + expression.addContextFunction("bossbar", -1, (c, t, lv) -> + { + CustomBossEvents bossBarManager = ((CarpetContext) c).server().getCustomBossEvents(); + if (lv.size() > 3) + { + throw new InternalExpressionException("'bossbar' accepts max three arguments"); + } + + if (lv.isEmpty()) + { + return ListValue.wrap(bossBarManager.getEvents().stream().map(CustomBossEvent::getTextId).map(ResourceLocation::toString).map(StringValue::of)); + } + + String id = lv.get(0).getString(); + ResourceLocation identifier = InputValidator.identifierOf(id); + + if (lv.size() == 1) + { + if (bossBarManager.get(identifier) != null) + { + return Value.FALSE; + } + return StringValue.of(bossBarManager.create(identifier, Component.literal(id)).getTextId().toString()); + } + + String property = lv.get(1).getString(); + + CustomBossEvent bossBar = bossBarManager.get(identifier); + if (bossBar == null) + { + return Value.NULL; + } + + Value propertyValue = (lv.size() == 3) ? lv.get(2) : null; + + switch (property) + { + case "color" -> { + if (propertyValue == null) + { + BossEvent.BossBarColor color = (bossBar).getColor(); + return color == null ? Value.NULL : StringValue.of(color.getName()); + } + BossEvent.BossBarColor color = BossEvent.BossBarColor.byName(propertyValue.getString()); + if (color == null) + { + return Value.NULL; + } + bossBar.setColor(BossEvent.BossBarColor.byName(propertyValue.getString())); + return Value.TRUE; + } + case "max" -> { + if (propertyValue == null) + { + return NumericValue.of(bossBar.getMax()); + } + if (!(propertyValue instanceof final NumericValue number)) + { + throw new InternalExpressionException("'bossbar' requires a number as the value for the property " + property); + } + bossBar.setMax(number.getInt()); + return Value.TRUE; + } + case "name" -> { + if (propertyValue == null) + { + return new FormattedTextValue(bossBar.getName()); + } + bossBar.setName(FormattedTextValue.getTextByValue(propertyValue)); + return Value.TRUE; + } + case "add_player" -> { + if (propertyValue == null) + { + throw new InternalExpressionException("Bossbar property " + property + " can't be queried, add a third parameter"); + } + if (propertyValue instanceof final ListValue list) + { + list.getItems().forEach(v -> { + ServerPlayer player = EntityValue.getPlayerByValue(((CarpetContext) c).server(), propertyValue); + if (player != null) + { + bossBar.addPlayer(player); + } + }); + return Value.TRUE; + } + ServerPlayer player = EntityValue.getPlayerByValue(((CarpetContext) c).server(), propertyValue); + if (player != null) + { + bossBar.addPlayer(player); + return Value.TRUE; + } + return Value.FALSE; + } + case "players" -> { + if (propertyValue == null) + { + return ListValue.wrap(bossBar.getPlayers().stream().map(EntityValue::new)); + } + if (propertyValue instanceof final ListValue list) + { + bossBar.removeAllPlayers(); + list.getItems().forEach(v -> { + ServerPlayer p = EntityValue.getPlayerByValue(((CarpetContext) c).server(), v); + if (p != null) + { + bossBar.addPlayer(p); + } + }); + return Value.TRUE; + } + ServerPlayer p = EntityValue.getPlayerByValue(((CarpetContext) c).server(), propertyValue); + bossBar.removeAllPlayers(); + if (p != null) + { + bossBar.addPlayer(p); + return Value.TRUE; + } + return Value.FALSE; + } + case "style" -> { + if (propertyValue == null) + { + return StringValue.of(bossBar.getOverlay().getName()); + } + BossEvent.BossBarOverlay style = BossEvent.BossBarOverlay.byName(propertyValue.getString()); + if (style == null) + { + throw new InternalExpressionException("'" + propertyValue.getString() + "' is not a valid value for property " + property); + } + bossBar.setOverlay(style); + return Value.TRUE; + } + case "value" -> { + if (propertyValue == null) + { + return NumericValue.of(bossBar.getValue()); + } + if (!(propertyValue instanceof final NumericValue number)) + { + throw new InternalExpressionException("'bossbar' requires a number as the value for the property " + property); + } + bossBar.setValue(number.getInt()); + return Value.TRUE; + } + case "visible" -> { + if (propertyValue == null) + { + return BooleanValue.of(bossBar.isVisible()); + } + bossBar.setVisible(propertyValue.getBoolean()); + return Value.TRUE; + } + case "remove" -> { + bossBarManager.remove(bossBar); + return Value.TRUE; + } + default -> throw new InternalExpressionException("Unknown bossbar property " + property); + } + }); + } +} + diff --git a/src/main/java/carpet/script/api/Threading.java b/src/main/java/carpet/script/api/Threading.java new file mode 100644 index 0000000..504838c --- /dev/null +++ b/src/main/java/carpet/script/api/Threading.java @@ -0,0 +1,78 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.Expression; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.ThreadValue; +import carpet.script.value.Value; +import net.minecraft.server.MinecraftServer; + +import java.util.concurrent.CompletionException; + +public class Threading +{ + public static void apply(Expression expression) + { + //"overridden" native call to cancel if on main thread + expression.addContextFunction("task_join", 1, (c, t, lv) -> { + if (((CarpetContext) c).server().isSameThread()) + { + throw new InternalExpressionException("'task_join' cannot be called from main thread to avoid deadlocks"); + } + Value v = lv.get(0); + if (!(v instanceof final ThreadValue tv)) + { + throw new InternalExpressionException("'task_join' could only be used with a task value"); + } + return tv.join(); + }); + + // has to be lazy due to deferred execution of the expression + expression.addLazyFunctionWithDelegation("task_dock", 1, false, true, (c, t, expr, tok, lv) -> { + CarpetContext cc = (CarpetContext) c; + MinecraftServer server = cc.server(); + if (server.isSameThread()) + { + return lv.get(0); // pass through for on thread tasks + } + Value[] result = new Value[]{Value.NULL}; + RuntimeException[] internal = new RuntimeException[]{null}; + try + { + ((CarpetContext) c).server().executeBlocking(() -> + { + 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 (ct, tt) -> ret; + // pass through placeholder + // implmenetation should dock the task on the main thread. + }); + } +} diff --git a/src/main/java/carpet/script/api/WorldAccess.java b/src/main/java/carpet/script/api/WorldAccess.java new file mode 100644 index 0000000..ae60ed0 --- /dev/null +++ b/src/main/java/carpet/script/api/WorldAccess.java @@ -0,0 +1,1805 @@ +package carpet.script.api; + +import carpet.script.CarpetContext; +import carpet.script.CarpetScriptServer; +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Fluff; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.utils.Colors; +import carpet.script.utils.FeatureGenerator; +import carpet.script.argument.BlockArgument; +import carpet.script.argument.Vector3Argument; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.utils.BiomeInfo; +import carpet.script.utils.InputValidator; +import carpet.script.utils.WorldTools; +import carpet.script.value.BlockValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.QuartPos; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.DensityFunction; +import net.minecraft.world.level.levelgen.DensityFunctions; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseRouter; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureType; +import org.apache.commons.lang3.mutable.MutableBoolean; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import net.minecraft.commands.arguments.item.ItemInput; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.protocol.game.ClientboundExplodePacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.util.SortedArraySet; +import net.minecraft.world.Clearable; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.village.poi.PoiManager; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.item.FallingBlockEntity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.DiggerItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.ShearsItem; +import net.minecraft.world.item.SwordItem; +import net.minecraft.world.item.TridentItem; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.biome.MultiNoiseBiomeSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.pathfinder.PathComputationType; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +import static carpet.script.utils.WorldTools.canHasChunk; + +public class WorldAccess +{ + private static final Map DIRECTION_MAP = Arrays.stream(Direction.values()).collect(Collectors.toMap(Direction::getName, Function.identity())); + + static + { + DIRECTION_MAP.put("y", Direction.UP); + DIRECTION_MAP.put("z", Direction.SOUTH); + DIRECTION_MAP.put("x", Direction.EAST); + } + + private static final Map> ticketTypes = Map.of( + "portal", TicketType.PORTAL, + "teleport", TicketType.POST_TELEPORT, + "unknown", TicketType.UNKNOWN + ); + // dummy entity for dummy requirements in the loot tables (see snowball) + private static FallingBlockEntity DUMMY_ENTITY = null; + + private static Value booleanStateTest( + Context c, + String name, + List params, + BiPredicate test + ) + { + CarpetContext cc = (CarpetContext) c; + if (params.isEmpty()) + { + throw new InternalExpressionException("'" + name + "' requires at least one parameter"); + } + if (params.get(0) instanceof final BlockValue bv) + { + return BooleanValue.of(test.test(bv.getBlockState(), bv.getPos())); + } + BlockValue block = BlockArgument.findIn(cc, params, 0).block; + return BooleanValue.of(test.test(block.getBlockState(), block.getPos())); + } + + private static Value stateStringQuery( + Context c, + String name, + List params, + BiFunction test + ) + { + CarpetContext cc = (CarpetContext) c; + if (params.isEmpty()) + { + throw new InternalExpressionException("'" + name + "' requires at least one parameter"); + } + if (params.get(0) instanceof final BlockValue bv) + { + return StringValue.of(test.apply(bv.getBlockState(), bv.getPos())); + } + BlockValue block = BlockArgument.findIn(cc, params, 0).block; + return StringValue.of(test.apply(block.getBlockState(), block.getPos())); + } + + private static Value genericStateTest( + Context c, + String name, + List params, + Fluff.TriFunction test + ) + { + CarpetContext cc = (CarpetContext) c; + if (params.isEmpty()) + { + throw new InternalExpressionException("'" + name + "' requires at least one parameter"); + } + if (params.get(0) instanceof final BlockValue bv) + { + try + { + return test.apply(bv.getBlockState(), bv.getPos(), cc.level()); + } + catch (NullPointerException ignored) + { + throw new InternalExpressionException("'" + name + "' function requires a block that is positioned in the world"); + } + } + BlockValue block = BlockArgument.findIn(cc, params, 0).block; + return test.apply(block.getBlockState(), block.getPos(), cc.level()); + } + + private static > BlockState setProperty(Property property, String name, String value, + BlockState bs) + { + Optional optional = property.getValue(value); + if (optional.isEmpty()) + { + throw new InternalExpressionException(value + " is not a valid value for property " + name); + } + return bs.setValue(property, optional.get()); + } + + private static void nullCheck(Value v, String name) + { + if (v.isNull()) + { + throw new IllegalArgumentException(name + " cannot be null"); + } + } + + private static float numberGetOrThrow(Value v) + { + double num = v.readDoubleNumber(); + if (Double.isNaN(num)) + { + throw new IllegalArgumentException(v.getString() + " needs to be a numeric value"); + } + return (float) num; + } + + private static void theBooYah(ServerLevel level) + { + synchronized (level) + { + level.getChunkSource().getGeneratorState().ensureStructuresGenerated(); + } + } + + public static void apply(Expression expression) + { + expression.addContextFunction("block", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.isEmpty()) + { + throw new InternalExpressionException("Block requires at least one parameter"); + } + BlockValue retval = BlockArgument.findIn(cc, lv, 0, true).block; + // fixing block state and data + retval.getBlockState(); + retval.getData(); + return retval; + }); + + expression.addContextFunction("block_data", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("Block requires at least one parameter"); + } + return NBTSerializableValue.of(BlockArgument.findIn((CarpetContext) c, lv, 0, true).block.getData()); + }); + + // poi_get(pos, radius?, type?, occupation?, column_mode?) + expression.addContextFunction("poi", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.isEmpty()) + { + throw new InternalExpressionException("'poi' requires at least one parameter"); + } + BlockArgument locator = BlockArgument.findIn(cc, lv, 0, false); + BlockPos pos = locator.block.getPos(); + PoiManager store = cc.level().getPoiManager(); + Registry poiReg = cc.registry(Registries.POINT_OF_INTEREST_TYPE); + if (lv.size() == locator.offset) + { + Optional> foo = store.getType(pos); + if (foo.isEmpty()) + { + return Value.NULL; + } + PoiType poiType = foo.get().value(); + + // this feels wrong, but I don't want to mix-in more than I really need to. + // also distance adds 0.5 to each point which screws up accurate distance calculations + // you shoudn't be using POI with that in mind anyways, so I am not worried about it. + PoiRecord poi = store.getInRange( + type -> type.value() == poiType, + pos, + 1, + PoiManager.Occupancy.ANY + ).filter(p -> p.getPos().equals(pos)).findFirst().orElse(null); + return poi == null ? Value.NULL : ListValue.of( + ValueConversions.of(poiReg.getKey(poi.getPoiType().value())), + new NumericValue(poiType.maxTickets() - Vanilla.PoiRecord_getFreeTickets(poi)) + ); + } + int radius = NumericValue.asNumber(lv.get(locator.offset)).getInt(); + if (radius < 0) + { + return ListValue.of(); + } + Predicate> condition = p -> true; + PoiManager.Occupancy status = PoiManager.Occupancy.ANY; + boolean inColumn = false; + if (locator.offset + 1 < lv.size()) + { + String poiType = lv.get(locator.offset + 1).getString().toLowerCase(Locale.ROOT); + if (!"any".equals(poiType)) + { + PoiType type = poiReg.getOptional(InputValidator.identifierOf(poiType)) + .orElseThrow(() -> new ThrowStatement(poiType, Throwables.UNKNOWN_POI)); + condition = tt -> tt.value() == type; + } + if (locator.offset + 2 < lv.size()) + { + String statusString = lv.get(locator.offset + 2).getString().toLowerCase(Locale.ROOT); + if ("occupied".equals(statusString)) + { + status = PoiManager.Occupancy.IS_OCCUPIED; + } + else if ("available".equals(statusString)) + { + status = PoiManager.Occupancy.HAS_SPACE; + } + else if (!("any".equals(statusString))) + { + throw new InternalExpressionException( + "Incorrect POI occupation status " + status + " use `any`, " + "`occupied` or `available`" + ); + } + if (locator.offset + 3 < lv.size()) + { + inColumn = lv.get(locator.offset + 3).getBoolean(); + } + } + } + Stream pois = inColumn ? + store.getInSquare(condition, pos, radius, status) : + store.getInRange(condition, pos, radius, status); + return ListValue.wrap(pois.sorted(Comparator.comparingDouble(p -> p.getPos().distSqr(pos))).map(p -> + ListValue.of( + ValueConversions.of(poiReg.getKey(p.getPoiType().value())), + new NumericValue(p.getPoiType().value().maxTickets() - Vanilla.PoiRecord_getFreeTickets(p)), + ValueConversions.of(p.getPos()) + ) + )); + }); + + //poi_set(pos, null) poi_set(pos, type, occupied?, + expression.addContextFunction("set_poi", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.isEmpty()) + { + throw new InternalExpressionException("'set_poi' requires at least one parameter"); + } + 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); + PoiManager store = cc.level().getPoiManager(); + if (poi.isNull()) + { // clear poi information + if (store.getType(pos).isEmpty()) + { + return Value.FALSE; + } + store.remove(pos); + return Value.TRUE; + } + String poiTypeString = poi.getString().toLowerCase(Locale.ROOT); + ResourceLocation resource = InputValidator.identifierOf(poiTypeString); + Registry poiReg = cc.registry(Registries.POINT_OF_INTEREST_TYPE); + PoiType type = poiReg.getOptional(resource) + .orElseThrow(() -> new ThrowStatement(poiTypeString, Throwables.UNKNOWN_POI)); + Holder holder = poiReg.getHolderOrThrow(ResourceKey.create(Registries.POINT_OF_INTEREST_TYPE, resource)); + + int occupancy = 0; + if (locator.offset + 1 < lv.size()) + { + occupancy = (int) NumericValue.asNumber(lv.get(locator.offset + 1)).getLong(); + if (occupancy < 0) + { + throw new InternalExpressionException("Occupancy cannot be negative"); + } + } + if (store.getType(pos).isPresent()) + { + store.remove(pos); + } + store.add(pos, holder); + // setting occupancy for a + // again - don't want to mix in unnecessarily - peeps not gonna use it that often so not worries about it. + if (occupancy > 0) + { + int finalO = occupancy; + store.getInSquare(tt -> tt.value() == type, pos, 1, PoiManager.Occupancy.ANY + ).filter(p -> p.getPos().equals(pos)).findFirst().ifPresent(p -> { + for (int i = 0; i < finalO; i++) + { + Vanilla.PoiRecord_callAcquireTicket(p); + } + }); + } + return Value.TRUE; + }); + + + expression.addContextFunction("weather", -1, (c, t, lv) -> { + ServerLevel world = ((CarpetContext) c).level(); + + if (lv.isEmpty())//cos it can thunder when raining or when clear. + { + return new StringValue(world.isThundering() ? "thunder" : (world.isRaining() ? "rain" : "clear")); + } + + Value weather = lv.get(0); + ServerLevelData worldProperties = Vanilla.ServerLevel_getWorldProperties(world); + if (lv.size() == 1) + { + return new NumericValue(switch (weather.getString().toLowerCase(Locale.ROOT)) + { + case "clear" -> worldProperties.getClearWeatherTime(); + case "rain" -> world.isRaining() ? worldProperties.getRainTime() : 0;//cos if not it gives 1 for some reason + case "thunder" -> world.isThundering() ? worldProperties.getThunderTime() : 0;//same dealio here + default -> throw new InternalExpressionException("Weather can only be 'clear', 'rain' or 'thunder'"); + }); + } + if (lv.size() == 2) + { + int ticks = NumericValue.asNumber(lv.get(1), "tick_time in 'weather'").getInt(); + switch (weather.getString().toLowerCase(Locale.ROOT)) + { + case "clear" -> world.setWeatherParameters(ticks, 0, false, false); + case "rain" -> world.setWeatherParameters(0, ticks, true, false); + case "thunder" -> world.setWeatherParameters( + 0, + ticks,//this is used to set thunder time, idk why... + true, + true + ); + default -> throw new InternalExpressionException("Weather can only be 'clear', 'rain' or 'thunder'"); + } + return NumericValue.of(ticks); + } + throw new InternalExpressionException("'weather' requires 0, 1 or 2 arguments"); + }); + + expression.addUnaryFunction("pos", v -> + { + if (v instanceof final BlockValue bv) + { + BlockPos pos = bv.getPos(); + if (pos == null) + { + throw new InternalExpressionException("Cannot fetch position of an unrealized block"); + } + return ValueConversions.of(pos); + } + if (v instanceof final EntityValue ev) + { + Entity e = ev.getEntity(); + if (e == null) + { + throw new InternalExpressionException("Null entity"); + } + return ValueConversions.of(e.position()); + } + throw new InternalExpressionException("'pos' works only with a block or an entity type"); + }); + + expression.addContextFunction("pos_offset", -1, (c, t, lv) -> + { + 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"); + } + String directionString = lv.get(locator.offset).getString(); + Direction dir = DIRECTION_MAP.get(directionString); + if (dir == null) + { + throw new InternalExpressionException("Unknown direction: " + directionString); + } + int howMuch = 1; + if (lv.size() > locator.offset + 1) + { + howMuch = (int) NumericValue.asNumber(lv.get(locator.offset + 1)).getLong(); + } + return ValueConversions.of(pos.relative(dir, howMuch)); + }); + + expression.addContextFunction("solid", -1, (c, t, lv) -> + genericStateTest(c, "solid", lv, (s, p, w) -> BooleanValue.of(s.isRedstoneConductor(w, p)))); // isSimpleFullBlock + + expression.addContextFunction("air", -1, (c, t, lv) -> + booleanStateTest(c, "air", lv, (s, p) -> s.isAir())); + + expression.addContextFunction("liquid", -1, (c, t, lv) -> + booleanStateTest(c, "liquid", lv, (s, p) -> !s.getFluidState().isEmpty())); + + expression.addContextFunction("flammable", -1, (c, t, lv) -> + booleanStateTest(c, "flammable", lv, (s, p) -> s.ignitedByLava())); + + expression.addContextFunction("transparent", -1, (c, t, lv) -> + booleanStateTest(c, "transparent", lv, (s, p) -> !s.isSolid())); + + /*this.expr.addContextFunction("opacity", -1, (c, t, lv) -> + genericStateTest(c, "opacity", lv, (s, p, w) -> new NumericValue(s.getOpacity(w, p)))); + + this.expr.addContextFunction("blocks_daylight", -1, (c, t, lv) -> + genericStateTest(c, "blocks_daylight", lv, (s, p, w) -> new NumericValue(s.propagatesSkylightDown(w, p))));*/ // investigate + + expression.addContextFunction("emitted_light", -1, (c, t, lv) -> + genericStateTest(c, "emitted_light", lv, (s, p, w) -> new NumericValue(s.getLightEmission()))); + + expression.addContextFunction("light", -1, (c, t, lv) -> + genericStateTest(c, "light", lv, (s, p, w) -> new NumericValue(Math.max(w.getBrightness(LightLayer.BLOCK, p), w.getBrightness(LightLayer.SKY, p))))); + + expression.addContextFunction("block_light", -1, (c, t, lv) -> + genericStateTest(c, "block_light", lv, (s, p, w) -> new NumericValue(w.getBrightness(LightLayer.BLOCK, p)))); + + expression.addContextFunction("sky_light", -1, (c, t, lv) -> + genericStateTest(c, "sky_light", lv, (s, p, w) -> new NumericValue(w.getBrightness(LightLayer.SKY, p)))); + + expression.addContextFunction("effective_light", -1, (c, t, lv) -> + genericStateTest(c, "effective_light", lv, (s, p, w) -> new NumericValue(w.getMaxLocalRawBrightness(p)))); + + expression.addContextFunction("see_sky", -1, (c, t, lv) -> + genericStateTest(c, "see_sky", lv, (s, p, w) -> BooleanValue.of(w.canSeeSky(p)))); + + expression.addContextFunction("brightness", -1, (c, t, lv) -> + genericStateTest(c, "brightness", lv, (s, p, w) -> new NumericValue(w.getLightLevelDependentMagicValue(p)))); + + expression.addContextFunction("hardness", -1, (c, t, lv) -> + genericStateTest(c, "hardness", lv, (s, p, w) -> new NumericValue(s.getDestroySpeed(w, p)))); + + expression.addContextFunction("blast_resistance", -1, (c, t, lv) -> + genericStateTest(c, "blast_resistance", lv, (s, p, w) -> new NumericValue(s.getBlock().getExplosionResistance()))); + + expression.addContextFunction("in_slime_chunk", -1, (c, t, lv) -> + { + BlockPos pos = BlockArgument.findIn((CarpetContext) c, lv, 0).block.getPos(); + ChunkPos chunkPos = new ChunkPos(pos); + return BooleanValue.of(WorldgenRandom.seedSlimeChunk( + chunkPos.x, chunkPos.z, + ((CarpetContext) c).level().getSeed(), + 987234911L + ).nextInt(10) == 0); + }); + + expression.addContextFunction("top", -1, (c, t, lv) -> + { + String type = lv.get(0).getString().toLowerCase(Locale.ROOT); + Heightmap.Types htype = switch (type) + { + //case "light": htype = Heightmap.Type.LIGHT_BLOCKING; break; //investigate + case "motion" -> Heightmap.Types.MOTION_BLOCKING; + case "terrain" -> Heightmap.Types.MOTION_BLOCKING_NO_LEAVES; + case "ocean_floor" -> Heightmap.Types.OCEAN_FLOOR; + case "surface" -> Heightmap.Types.WORLD_SURFACE; + default -> throw new InternalExpressionException("Unknown heightmap type: " + type); + }; + BlockArgument locator = BlockArgument.findIn((CarpetContext) c, lv, 1); + BlockPos pos = locator.block.getPos(); + int x = pos.getX(); + int z = pos.getZ(); + return new NumericValue(((CarpetContext) c).level().getChunk(x >> 4, z >> 4).getHeight(htype, x & 15, z & 15) + 1); + }); + + expression.addContextFunction("loaded", -1, (c, t, lv) -> + BooleanValue.of((((CarpetContext) c).level().hasChunkAt(BlockArgument.findIn((CarpetContext) c, lv, 0).block.getPos())))); + + // Deprecated, use loaded_status as more indicative + expression.addContextFunction("loaded_ep", -1, (c, t, lv) -> + { + c.host.issueDeprecation("loaded_ep(...)"); + BlockPos pos = BlockArgument.findIn((CarpetContext) c, lv, 0).block.getPos(); + return BooleanValue.of(((CarpetContext) c).level().isPositionEntityTicking(pos)); + }); + + expression.addContextFunction("loaded_status", -1, (c, t, lv) -> + { + BlockPos pos = BlockArgument.findIn((CarpetContext) c, lv, 0).block.getPos(); + LevelChunk chunk = ((CarpetContext) c).level().getChunkSource().getChunk(pos.getX() >> 4, pos.getZ() >> 4, false); + return chunk == null ? Value.ZERO : new NumericValue(chunk.getFullStatus().ordinal()); + }); + + expression.addContextFunction("is_chunk_generated", -1, (c, t, lv) -> + { + BlockArgument locator = BlockArgument.findIn((CarpetContext) c, lv, 0); + BlockPos pos = locator.block.getPos(); + boolean force = false; + if (lv.size() > locator.offset) + { + force = lv.get(locator.offset).getBoolean(); + } + return BooleanValue.of(canHasChunk(((CarpetContext) c).level(), new ChunkPos(pos), null, force)); + }); + + expression.addContextFunction("generation_status", -1, (c, t, lv) -> + { + BlockArgument blockArgument = BlockArgument.findIn((CarpetContext) c, lv, 0); + BlockPos pos = blockArgument.block.getPos(); + boolean forceLoad = false; + if (lv.size() > blockArgument.offset) + { + forceLoad = lv.get(blockArgument.offset).getBoolean(); + } + ChunkAccess chunk = ((CarpetContext) c).level().getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, forceLoad); + return chunk == null ? Value.NULL : ValueConversions.of(BuiltInRegistries.CHUNK_STATUS.getKey(chunk.getPersistedStatus())); + }); + + expression.addContextFunction("chunk_tickets", -1, (c, t, lv) -> + { + ServerLevel world = ((CarpetContext) c).level(); + DistanceManager foo = world.getChunkSource().chunkMap.getDistanceManager(); + Long2ObjectOpenHashMap>> levelTickets = Vanilla.ChunkTicketManager_getTicketsByPosition(foo); + + List res = new ArrayList<>(); + if (lv.isEmpty()) + { + for (long key : levelTickets.keySet()) + { + ChunkPos chpos = new ChunkPos(key); + for (Ticket ticket : levelTickets.get(key)) + { + res.add(ListValue.of( + new StringValue(ticket.getType().toString()), + new NumericValue(33 - ticket.getTicketLevel()), + new NumericValue(chpos.x), + new NumericValue(chpos.z) + )); + } + } + } + else + { + BlockArgument blockArgument = BlockArgument.findIn((CarpetContext) c, lv, 0); + BlockPos pos = blockArgument.block.getPos(); + SortedArraySet> tickets = levelTickets.get(new ChunkPos(pos).toLong()); + if (tickets != null) + { + for (Ticket ticket : tickets) + { + res.add(ListValue.of( + new StringValue(ticket.getType().toString()), + new NumericValue(33 - ticket.getTicketLevel()) + )); + } + } + } + res.sort(Comparator.comparing(e -> ((ListValue) e).getItems().get(1)).reversed()); + return ListValue.wrap(res); + }); + + expression.addContextFunction("suffocates", -1, (c, t, lv) -> + genericStateTest(c, "suffocates", lv, (s, p, w) -> BooleanValue.of(s.isSuffocating(w, p)))); // canSuffocate + + expression.addContextFunction("power", -1, (c, t, lv) -> + genericStateTest(c, "power", lv, (s, p, w) -> new NumericValue(w.getBestNeighborSignal(p)))); + + expression.addContextFunction("ticks_randomly", -1, (c, t, lv) -> + booleanStateTest(c, "ticks_randomly", lv, (s, p) -> s.isRandomlyTicking())); + + expression.addContextFunction("update", -1, (c, t, lv) -> + booleanStateTest(c, "update", lv, (s, p) -> + { + ((CarpetContext) c).level().neighborChanged(p, s.getBlock(), p); + return true; + })); + + expression.addContextFunction("block_tick", -1, (c, t, lv) -> + booleanStateTest(c, "block_tick", lv, (s, p) -> + { + ServerLevel w = ((CarpetContext) c).level(); + s.randomTick(w, p, w.random); + return true; + })); + + expression.addContextFunction("random_tick", -1, (c, t, lv) -> + booleanStateTest(c, "random_tick", lv, (s, p) -> + { + ServerLevel w = ((CarpetContext) c).level(); + if (s.isRandomlyTicking() || s.getFluidState().isRandomlyTicking()) + { + s.randomTick(w, p, w.random); + } + return true; + })); + + // lazy cause its parked execution + expression.addLazyFunction("without_updates", 1, (c, t, lv) -> + { + if (Carpet.getImpendingFillSkipUpdates().get()) + { + return lv.get(0); + } + Value[] result = new Value[]{Value.NULL}; + ((CarpetContext) c).server().executeBlocking(() -> + { + ThreadLocal skipUpdates = Carpet.getImpendingFillSkipUpdates(); + boolean previous = skipUpdates.get(); + try + { + skipUpdates.set(true); + result[0] = lv.get(0).evalValue(c, t); + } + finally + { + skipUpdates.set(previous); + } + }); + return (cc, tt) -> result[0]; + }); + + expression.addContextFunction("set", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + ServerLevel world = cc.level(); + 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()); + CompoundTag data = null; + if (lv.size() > sourceLocator.offset) + { + List args = new ArrayList<>(); + for (int i = sourceLocator.offset, m = lv.size(); i < m; i++) + { + args.add(lv.get(i)); + } + if (args.get(0) instanceof final ListValue list) + { + if (args.size() == 2 && NBTSerializableValue.fromValue(args.get(1)) instanceof final NBTSerializableValue nbtsv) + { + data = nbtsv.getCompoundTag(); + } + args = list.getItems(); + } + else if (args.get(0) instanceof final MapValue map) + { + if (args.size() == 2 && NBTSerializableValue.fromValue(args.get(1)) instanceof final NBTSerializableValue nbtsv) + { + data = nbtsv.getCompoundTag(); + } + Map state = map.getMap(); + List mapargs = new ArrayList<>(); + state.forEach((k, v) -> { + mapargs.add(k); + mapargs.add(v); + }); + args = mapargs; + } + else + { + if ((args.size() & 1) == 1 && NBTSerializableValue.fromValue(args.get(args.size() - 1)) instanceof final NBTSerializableValue nbtsv) + { + data = nbtsv.getCompoundTag(); + } + } + StateDefinition states = sourceBlockState.getBlock().getStateDefinition(); + for (int i = 0; i < args.size() - 1; i += 2) + { + String paramString = args.get(i).getString(); + Property property = states.getProperty(paramString); + if (property == null) + { + throw new InternalExpressionException("Property " + paramString + " doesn't apply to " + sourceLocator.block.getString()); + } + String paramValue = args.get(i + 1).getString(); + sourceBlockState = setProperty(property, paramString, paramValue, sourceBlockState); + } + } + + if (data == null) + { + data = sourceLocator.block.getData(); + } + CompoundTag finalData = data; + + if (sourceBlockState == targetBlockState && data == null) + { + return Value.FALSE; + } + BlockState finalSourceBlockState = sourceBlockState; + BlockPos targetPos = targetLocator.block.getPos(); + Boolean[] result = new Boolean[]{true}; + cc.server().executeBlocking(() -> + { + Clearable.tryClear(world.getBlockEntity(targetPos)); + boolean success = world.setBlock(targetPos, finalSourceBlockState, 2); + if (finalData != null) + { + BlockEntity be = world.getBlockEntity(targetPos); + if (be != null) + { + CompoundTag destTag = finalData.copy(); + destTag.putInt("x", targetPos.getX()); + destTag.putInt("y", targetPos.getY()); + destTag.putInt("z", targetPos.getZ()); + be.loadWithComponents(destTag, world.registryAccess()); + be.setChanged(); + success = true; + } + } + result[0] = success; + }); + return !result[0] ? Value.FALSE : new BlockValue(finalSourceBlockState, world, targetLocator.block.getPos()); + }); + + expression.addContextFunction("destroy", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + RegistryAccess regs = cc.registryAccess(); + ServerLevel world = cc.level(); + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + BlockState state = locator.block.getBlockState(); + if (state.isAir()) + { + return Value.FALSE; + } + BlockPos where = locator.block.getPos(); + BlockEntity be = world.getBlockEntity(where); + long how = 0; + Item item = Items.DIAMOND_PICKAXE; + boolean playerBreak = false; + if (lv.size() > locator.offset) + { + Value val = lv.get(locator.offset); + if (val instanceof final NumericValue number) + { + how = number.getLong(); + } + else + { + playerBreak = true; + String itemString = val.getString(); + item = cc.registry(Registries.ITEM).getOptional(InputValidator.identifierOf(itemString)) + .orElseThrow(() -> new ThrowStatement(itemString, Throwables.UNKNOWN_ITEM)); + } + } + CompoundTag tag = null; + if (lv.size() > locator.offset + 1) + { + if (!playerBreak) + { + throw new InternalExpressionException("tag is not necessary with 'destroy' with no item"); + } + Value tagValue = lv.get(locator.offset + 1); + if (!tagValue.isNull()) + { + tag = tagValue instanceof final NBTSerializableValue nbtsv + ? nbtsv.getCompoundTag() + : NBTSerializableValue.parseStringOrFail(tagValue.getString()).getCompoundTag(); + } + } + ItemStack tool; + if (tag != null) + { + tool = ItemStack.parseOptional(regs, tag); + } + else + { + tool = new ItemStack(item, 1); + } + if (playerBreak && state.getDestroySpeed(world, where) < 0.0) + { + return Value.FALSE; + } + boolean removed = world.removeBlock(where, false); + if (!removed) + { + return Value.FALSE; + } + world.levelEvent(null, 2001, where, Block.getId(state)); + + final MutableBoolean toolBroke = new MutableBoolean(false); + boolean dropLoot = true; + if (playerBreak) + { + boolean isUsingEffectiveTool = !state.requiresCorrectToolForDrops() || tool.isCorrectToolForDrops(state); + //postMine() durability from item classes + float hardness = state.getDestroySpeed(world, where); + int damageAmount = 0; + if ((item instanceof DiggerItem && hardness > 0.0) || item instanceof ShearsItem) + { + damageAmount = 1; + } + else if (item instanceof TridentItem || item instanceof SwordItem) + { + damageAmount = 2; + } + final int finalDamageAmount = damageAmount; + tool.hurtAndBreak(damageAmount, world, null, (i) -> { if (finalDamageAmount > 0) toolBroke.setTrue(); } ); + if (!isUsingEffectiveTool) + { + dropLoot = false; + } + } + + if (dropLoot) + { + if (how < 0 || (tag != null && EnchantmentHelper.getItemEnchantmentLevel(world.registryAccess().registryOrThrow(Registries.ENCHANTMENT).getHolderOrThrow(Enchantments.SILK_TOUCH), tool) > 0)) + { + Block.popResource(world, where, new ItemStack(state.getBlock())); + } + else + { + if (how > 0) + { + tool.enchant(world.registryAccess().registryOrThrow(Registries.ENCHANTMENT).getHolderOrThrow(Enchantments.FORTUNE), (int) how); + } + if (DUMMY_ENTITY == null) + { + DUMMY_ENTITY = new FallingBlockEntity(EntityType.FALLING_BLOCK, null); + } + Block.dropResources(state, world, where, be, DUMMY_ENTITY, tool); + } + } + if (!playerBreak) // no tool info - block brokwn + { + return Value.TRUE; + } + if (toolBroke.booleanValue()) + { + return Value.NULL; + } + return new NBTSerializableValue(() -> tool.saveOptional(regs)); + + }); + + expression.addContextFunction("harvest", -1, (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'harvest' takes at least 2 parameters: entity and block, or position, to harvest"); + } + CarpetContext cc = (CarpetContext) c; + Level world = cc.level(); + Value entityValue = lv.get(0); + if (!(entityValue instanceof final EntityValue ev)) + { + return Value.FALSE; + } + Entity e = ev.getEntity(); + if (!(e instanceof final ServerPlayer player)) + { + return Value.FALSE; + } + BlockArgument locator = BlockArgument.findIn(cc, lv, 1); + BlockPos where = locator.block.getPos(); + BlockState state = locator.block.getBlockState(); + Block block = state.getBlock(); + boolean success = false; + if (!((block == Blocks.BEDROCK || block == Blocks.BARRIER) && player.gameMode.isSurvival())) + { + success = player.gameMode.destroyBlock(where); + } + if (success) + { + world.levelEvent(null, 2001, where, Block.getId(state)); + } + return BooleanValue.of(success); + }); + + expression.addContextFunction("create_explosion", -1, (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'create_explosion' requires at least a position to explode"); + } + CarpetContext cc = (CarpetContext) c; + float powah = 4.0f; + Explosion.BlockInteraction mode = Explosion.BlockInteraction.DESTROY; // should probably read the gamerule for default behaviour + boolean createFire = false; + Entity source = null; + LivingEntity attacker = null; + Vector3Argument location = Vector3Argument.findIn(lv, 0, false, true); + Vec3 pos = location.vec; + if (lv.size() > location.offset) + { + powah = NumericValue.asNumber(lv.get(location.offset), "explosion power").getFloat(); + if (powah < 0) + { + throw new InternalExpressionException("Explosion power cannot be negative"); + } + if (lv.size() > location.offset + 1) + { + String strval = lv.get(location.offset + 1).getString(); + try + { + mode = Explosion.BlockInteraction.valueOf(strval.toUpperCase(Locale.ROOT)); + } + catch (IllegalArgumentException ile) + { + throw new InternalExpressionException("Illegal explosions block behaviour: " + strval); + } + if (lv.size() > location.offset + 2) + { + createFire = lv.get(location.offset + 2).getBoolean(); + if (lv.size() > location.offset + 3) + { + Value enVal = lv.get(location.offset + 3); + if (!enVal.isNull()) + { + if (enVal instanceof final EntityValue ev) + { + source = ev.getEntity(); + } + else + { + throw new InternalExpressionException("Fourth parameter of the explosion has to be an entity, not " + enVal.getTypeString()); + } + } + if (lv.size() > location.offset + 4) + { + enVal = lv.get(location.offset + 4); + if (!enVal.isNull()) + { + if (enVal instanceof final EntityValue ev) + { + Entity attackingEntity = ev.getEntity(); + if (attackingEntity instanceof final LivingEntity le) + { + attacker = le; + } + else + { + throw new InternalExpressionException("Attacking entity needs to be a living thing, " + + ValueConversions.of(cc.registry(Registries.ENTITY_TYPE).getKey(attackingEntity.getType())).getString() + " ain't it."); + } + } + else + { + throw new InternalExpressionException("Fifth parameter of the explosion has to be a living entity, not " + enVal.getTypeString()); + } + } + } + } + } + } + } + LivingEntity theAttacker = attacker; + float thePowah = powah; + + // copy of ServerWorld.createExplosion #TRACK# + Explosion explosion = new Explosion(cc.level(), source, null, null, pos.x, pos.y, pos.z, powah, createFire, mode, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE) + { + @Override + @Nullable + public + LivingEntity getIndirectSourceEntity() + { + return theAttacker; + } + }; + explosion.explode(); + explosion.finalizeExplosion(false); + if (mode == Explosion.BlockInteraction.KEEP) + { + explosion.clearToBlow(); + } + Explosion.BlockInteraction finalMode = mode; + cc.level().players().forEach(spe -> { + if (spe.distanceToSqr(pos) < 4096.0D) + { + spe.connection.send(new ClientboundExplodePacket(pos.x, pos.y, pos.z, thePowah, explosion.getToBlow(), explosion.getHitPlayers().get(spe), finalMode, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE)); + } + }); + return Value.TRUE; + }); + + // TODO rename to use_item + expression.addContextFunction("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).getString(); + Vector3Argument locator = Vector3Argument.findIn(lv, 1); + ItemStack stackArg = NBTSerializableValue.parseItem(itemString, cc.registryAccess()); + BlockPos where = BlockPos.containing(locator.vec); + // Paintings throw an exception if their direction is vertical, therefore we change the default here + String facing = lv.size() > locator.offset + ? lv.get(locator.offset).getString() + : stackArg.getItem() != Items.PAINTING ? "up" : "north"; + boolean sneakPlace = false; + if (lv.size() > locator.offset + 1) + { + sneakPlace = lv.get(locator.offset + 1).getBoolean(); + } + + BlockValue.PlacementContext ctx = BlockValue.PlacementContext.from(cc.level(), where, facing, sneakPlace, stackArg); + + if (!(stackArg.getItem() instanceof final BlockItem blockItem)) + { + InteractionResult useResult = ctx.getItemInHand().useOn(ctx); + if (useResult == InteractionResult.CONSUME || useResult == InteractionResult.SUCCESS) + { + return Value.TRUE; + } + } + else + { // not sure we need special case for block items, since useOnBlock can do that as well + if (!ctx.canPlace()) + { + return Value.FALSE; + } + BlockState placementState = blockItem.getBlock().getStateForPlacement(ctx); + if (placementState != null) + { + Level level = ctx.getLevel(); + if (placementState.canSurvive(level, where)) + { + level.setBlock(where, placementState, 2); + SoundType blockSoundGroup = placementState.getSoundType(); + level.playSound(null, where, blockSoundGroup.getPlaceSound(), SoundSource.BLOCKS, (blockSoundGroup.getVolume() + 1.0F) / 2.0F, blockSoundGroup.getPitch() * 0.8F); + return Value.TRUE; + } + } + } + return Value.FALSE; + }); + + expression.addContextFunction("blocks_movement", -1, (c, t, lv) -> + booleanStateTest(c, "blocks_movement", lv, (s, p) -> + !s.isPathfindable(PathComputationType.LAND))); + + expression.addContextFunction("block_sound", -1, (c, t, lv) -> + stateStringQuery(c, "block_sound", lv, (s, p) -> + Colors.soundName.get(s.getSoundType()))); + + expression.addContextFunction("material", -1, (c, t, lv) -> { + c.host.issueDeprecation("material(...)"); // deprecated for block_state() + return StringValue.of("unknown"); + }); + + expression.addContextFunction("map_colour", -1, (c, t, lv) -> + stateStringQuery(c, "map_colour", lv, (s, p) -> + Colors.mapColourName.get(s.getMapColor(((CarpetContext) c).level(), p)))); + + // Deprecated for block_state() + expression.addContextFunction("property", -1, (c, t, lv) -> + { + c.host.issueDeprecation("property(...)"); + 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"); + } + String tag = lv.get(locator.offset).getString(); + StateDefinition states = state.getBlock().getStateDefinition(); + Property property = states.getProperty(tag); + return property == null ? Value.NULL : new StringValue(state.getValue(property).toString().toLowerCase(Locale.ROOT)); + }); + + // Deprecated for block_state() + expression.addContextFunction("block_properties", -1, (c, t, lv) -> + { + c.host.issueDeprecation("block_properties(...)"); + BlockArgument locator = BlockArgument.findIn((CarpetContext) c, lv, 0); + BlockState state = locator.block.getBlockState(); + StateDefinition states = state.getBlock().getStateDefinition(); + return ListValue.wrap(states.getProperties().stream().map( + p -> new StringValue(p.getName())) + ); + }); + + // block_state(block) + // block_state(block, property) + expression.addContextFunction("block_state", -1, (c, t, lv) -> + { + BlockArgument locator = BlockArgument.findIn((CarpetContext) c, lv, 0, true); + BlockState state = locator.block.getBlockState(); + StateDefinition states = state.getBlock().getStateDefinition(); + if (locator.offset == lv.size()) + { + Map properties = new HashMap<>(); + for (Property p : states.getProperties()) + { + properties.put(StringValue.of(p.getName()), ValueConversions.fromProperty(state, p)); + } + return MapValue.wrap(properties); + } + String tag = lv.get(locator.offset).getString(); + Property property = states.getProperty(tag); + return property == null ? Value.NULL : ValueConversions.fromProperty(state, property); + }); + + expression.addContextFunction("block_list", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + Registry blocks = cc.registry(Registries.BLOCK); + if (lv.isEmpty()) + { + return ListValue.wrap(blocks.holders().map(blockReference -> ValueConversions.of(blockReference.key().location()))); + } + ResourceLocation tag = InputValidator.identifierOf(lv.get(0).getString()); + Optional> tagset = blocks.getTag(TagKey.create(Registries.BLOCK, tag)); + return tagset.isEmpty() ? Value.NULL : ListValue.wrap(tagset.get().stream().map(b -> ValueConversions.of(blocks.getKey(b.value())))); + }); + + expression.addContextFunction("block_tags", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + Registry blocks = cc.registry(Registries.BLOCK); + if (lv.isEmpty()) + { + return ListValue.wrap(blocks.getTagNames().map(ValueConversions::of)); + } + BlockArgument blockLocator = BlockArgument.findIn(cc, lv, 0, true); + if (blockLocator.offset == lv.size()) + { + Block target = blockLocator.block.getBlockState().getBlock(); + return ListValue.wrap(blocks.getTags().filter(e -> e.getSecond().stream().anyMatch(h -> (h.value() == target))).map(e -> ValueConversions.of(e.getFirst()))); + } + String tag = lv.get(blockLocator.offset).getString(); + Optional> tagSet = blocks.getTag(TagKey.create(Registries.BLOCK, InputValidator.identifierOf(tag))); + return tagSet.isEmpty() ? Value.NULL : BooleanValue.of(blockLocator.block.getBlockState().is(tagSet.get())); + }); + + expression.addContextFunction("biome", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + ServerLevel world = cc.level(); + if (lv.isEmpty()) + { + return ListValue.wrap(cc.registry(Registries.BIOME).holders().map(biomeReference -> ValueConversions.of(biomeReference.key().location()))); + } + + Biome biome; + BiomeSource biomeSource = world.getChunkSource().getGenerator().getBiomeSource(); + if (lv.size() == 1 + && lv.get(0) instanceof final MapValue map + && biomeSource instanceof final MultiNoiseBiomeSource mnbs + ) + { + Value temperature = map.get(new StringValue("temperature")); + nullCheck(temperature, "temperature"); + + Value humidity = map.get(new StringValue("humidity")); + nullCheck(humidity, "humidity"); + + Value continentalness = map.get(new StringValue("continentalness")); + nullCheck(continentalness, "continentalness"); + + Value erosion = map.get(new StringValue("erosion")); + nullCheck(erosion, "erosion"); + + Value depth = map.get(new StringValue("depth")); + nullCheck(depth, "depth"); + + Value weirdness = map.get(new StringValue("weirdness")); + nullCheck(weirdness, "weirdness"); + + Climate.TargetPoint point = new Climate.TargetPoint( + Climate.quantizeCoord(numberGetOrThrow(temperature)), + Climate.quantizeCoord(numberGetOrThrow(humidity)), + Climate.quantizeCoord(numberGetOrThrow(continentalness)), + Climate.quantizeCoord(numberGetOrThrow(erosion)), + Climate.quantizeCoord(numberGetOrThrow(depth)), + Climate.quantizeCoord(numberGetOrThrow(weirdness)) + ); + biome = mnbs.getNoiseBiome(point).value(); + ResourceLocation biomeId = cc.registry(Registries.BIOME).getKey(biome); + return NBTSerializableValue.nameFromRegistryId(biomeId); + } + + BlockArgument locator = BlockArgument.findIn(cc, lv, 0, false, false, true); + + if (locator.replacement != null) + { + biome = world.registryAccess().registryOrThrow(Registries.BIOME).get(InputValidator.identifierOf(locator.replacement)); + if (biome == null) + { + throw new ThrowStatement(locator.replacement, Throwables.UNKNOWN_BIOME); + } + } + else + { + biome = world.getBiome(locator.block.getPos()).value(); + } + // in locatebiome + if (locator.offset == lv.size()) + { + ResourceLocation biomeId = cc.registry(Registries.BIOME).getKey(biome); + return NBTSerializableValue.nameFromRegistryId(biomeId); + } + String biomeFeature = lv.get(locator.offset).getString(); + BiFunction featureProvider = BiomeInfo.biomeFeatures.get(biomeFeature); + if (featureProvider == null) + { + throw new InternalExpressionException("Unknown biome feature: " + biomeFeature); + } + return featureProvider.apply(world, biome); + }); + + expression.addContextFunction("set_biome", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + 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).getString(); + // from locatebiome command code + Holder biome = cc.registry(Registries.BIOME).getHolder(ResourceKey.create(Registries.BIOME, InputValidator.identifierOf(biomeName))) + .orElseThrow(() -> new ThrowStatement(biomeName, Throwables.UNKNOWN_BIOME)); + boolean doImmediateUpdate = true; + if (lv.size() > locator.offset + 1) + { + doImmediateUpdate = lv.get(locator.offset + 1).getBoolean(); + } + ServerLevel world = cc.level(); + BlockPos pos = locator.block.getPos(); + ChunkAccess chunk = world.getChunk(pos); // getting level chunk instead of protochunk with biomes + int biomeX = QuartPos.fromBlock(pos.getX()); + int biomeY = QuartPos.fromBlock(pos.getY()); + int biomeZ = QuartPos.fromBlock(pos.getZ()); + try + { + int i = QuartPos.fromBlock(chunk.getMinBuildHeight()); + int j = i + QuartPos.fromBlock(chunk.getHeight()) - 1; + int k = Mth.clamp(biomeY, i, j); + int l = chunk.getSectionIndex(QuartPos.toBlock(k)); + // accessing outside of the interface - might be dangerous in the future. + ((PalettedContainer>) chunk.getSection(l).getBiomes()).set(biomeX & 3, k & 3, biomeZ & 3, biome); + } + catch (Throwable var8) + { + return Value.FALSE; + } + if (doImmediateUpdate) + { + WorldTools.forceChunkUpdate(pos, world); + } + chunk.setUnsaved(true); + return Value.TRUE; + }); + + expression.addContextFunction("reload_chunk", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + BlockPos pos = BlockArgument.findIn(cc, lv, 0).block.getPos(); + ServerLevel world = cc.level(); + cc.server().executeBlocking(() -> WorldTools.forceChunkUpdate(pos, world)); + return Value.TRUE; + }); + + expression.addContextFunction("structure_references", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + ServerLevel world = cc.level(); + BlockPos pos = locator.block.getPos(); + Map references = world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); + Registry reg = cc.registry(Registries.STRUCTURE); + if (lv.size() == locator.offset) + { + return ListValue.wrap(references.entrySet().stream(). + filter(e -> e.getValue() != null && !e.getValue().isEmpty()). + map(e -> NBTSerializableValue.nameFromRegistryId(reg.getKey(e.getKey()))) + ); + } + String simpleStructureName = lv.get(locator.offset).getString().toLowerCase(Locale.ROOT); + Structure structureName = reg.get(InputValidator.identifierOf(simpleStructureName)); + if (structureName == null) + { + return Value.NULL; + } + LongSet structureReferences = references.get(structureName); + if (structureReferences == null || structureReferences.isEmpty()) + { + return ListValue.of(); + } + return ListValue.wrap(structureReferences.longStream().mapToObj(l -> ListValue.of( + new NumericValue(16L * ChunkPos.getX(l)), + Value.ZERO, + new NumericValue(16L * ChunkPos.getZ(l))))); + }); + + expression.addContextFunction("structure_eligibility", -1, (c, t, lv) -> + {// TODO rename structureName to class + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + + ServerLevel world = cc.level(); + + // well, because + theBooYah(world); + + BlockPos pos = locator.block.getPos(); + List structure = new ArrayList<>(); + boolean needSize = false; + boolean singleOutput = false; + Registry reg = cc.registry(Registries.STRUCTURE); + if (lv.size() > locator.offset) + { + Value requested = lv.get(locator.offset); + if (!requested.isNull()) + { + String reqString = requested.getString(); + ResourceLocation id = InputValidator.identifierOf(reqString); + Structure requestedStructure = reg.get(id); + if (requestedStructure != null) + { + singleOutput = true; + structure.add(requestedStructure); + } + else + { + StructureType sss = cc.registry(Registries.STRUCTURE_TYPE).get(id); + reg.entrySet().stream().filter(e -> e.getValue().type() == sss).forEach(e -> structure.add(e.getValue())); + } + if (structure.isEmpty()) + { + throw new ThrowStatement(reqString, Throwables.UNKNOWN_STRUCTURE); + } + + } + else + { + structure.addAll(reg.entrySet().stream().map(Map.Entry::getValue).toList()); + } + if (lv.size() > locator.offset + 1) + { + needSize = lv.get(locator.offset + 1).getBoolean(); + } + } + else + { + structure.addAll(reg.entrySet().stream().map(Map.Entry::getValue).toList()); + } + if (singleOutput) + { + StructureStart start = FeatureGenerator.shouldStructureStartAt(world, pos, structure.get(0), needSize); + return start == null ? Value.NULL : !needSize ? Value.TRUE : ValueConversions.of(start, cc.registryAccess()); + } + Map ret = new HashMap<>(); + for (Structure str : structure) + { + StructureStart start; + try + { + start = FeatureGenerator.shouldStructureStartAt(world, pos, str, needSize); + } + catch (NullPointerException npe) + { + CarpetScriptServer.LOG.error("Failed to detect structure: " + reg.getKey(str)); + start = null; + } + + if (start == null) + { + continue; + } + + Value key = NBTSerializableValue.nameFromRegistryId(reg.getKey(str)); + ret.put(key, (!needSize) ? Value.NULL : ValueConversions.of(start, cc.registryAccess())); + } + return MapValue.wrap(ret); + }); + + expression.addContextFunction("structures", -1, (c, t, lv) -> { + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + + ServerLevel world = cc.level(); + BlockPos pos = locator.block.getPos(); + Map structures = world.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.STRUCTURE_STARTS).getAllStarts(); + Registry reg = cc.registry(Registries.STRUCTURE); + if (lv.size() == locator.offset) + { + Map structureList = new HashMap<>(); + for (Map.Entry entry : structures.entrySet()) + { + StructureStart start = entry.getValue(); + if (start == StructureStart.INVALID_START) + { + continue; + } + BoundingBox box = start.getBoundingBox(); + structureList.put( + NBTSerializableValue.nameFromRegistryId(reg.getKey(entry.getKey())), + ValueConversions.of(box) + ); + } + return MapValue.wrap(structureList); + } + String structureName = lv.get(locator.offset).getString().toLowerCase(Locale.ROOT); + return ValueConversions.of(structures.get(reg.get(InputValidator.identifierOf(structureName))), cc.registryAccess()); + }); + + expression.addContextFunction("set_structure", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + + ServerLevel world = cc.level(); + BlockPos pos = locator.block.getPos(); + + if (lv.size() == locator.offset) + { + throw new InternalExpressionException("'set_structure requires at least position and a structure name"); + } + String structureName = lv.get(locator.offset).getString().toLowerCase(Locale.ROOT); + Structure configuredStructure = FeatureGenerator.resolveConfiguredStructure(structureName, world, pos); + if (configuredStructure == null) + { + throw new ThrowStatement(structureName, Throwables.UNKNOWN_STRUCTURE); + } + // good 'ol pointer + Value[] result = new Value[]{Value.NULL}; + // technically a world modification. Even if we could let it slide, we will still park it + ((CarpetContext) c).server().executeBlocking(() -> + { + Map structures = world.getChunk(pos).getAllStarts(); + if (lv.size() == locator.offset + 1) + { + boolean res = FeatureGenerator.plopGrid(configuredStructure, ((CarpetContext) c).level(), locator.block.getPos()); + result[0] = res ? Value.TRUE : Value.FALSE; + return; + } + Value newValue = lv.get(locator.offset + 1); + if (newValue.isNull()) // remove structure + { + if (!structures.containsKey(configuredStructure)) + { + return; + } + StructureStart start = structures.get(configuredStructure); + ChunkPos structureChunkPos = start.getChunkPos(); + BoundingBox box = start.getBoundingBox(); + for (int chx = box.minX() / 16; chx <= box.maxX() / 16; chx++) // minx maxx + { + for (int chz = box.minZ() / 16; chz <= box.maxZ() / 16; chz++) //minZ maxZ + { + ChunkPos chpos = new ChunkPos(chx, chz); + // getting a chunk will convert it to full, allowing to modify references + Map references = + world.getChunk(chpos.getWorldPosition()).getAllReferences(); + if (references.containsKey(configuredStructure) && references.get(configuredStructure) != null) + { + references.get(configuredStructure).remove(structureChunkPos.toLong()); + } + } + } + structures.remove(configuredStructure); + result[0] = Value.TRUE; + } + }); + return result[0]; // preventing from lazy evaluating of the result in case a future completes later + }); + + // todo maybe enable chunk blending? + expression.addContextFunction("reset_chunk", -1, (c, t, lv) -> + { + return Value.NULL; + /* + CarpetContext cc = (CarpetContext) c; + List requestedChunks = new ArrayList<>(); + if (lv.size() == 1) + { + //either one block or list of chunks + Value first = lv.get(0); + if (first instanceof final ListValue list) + { + List listVal = list.getItems(); + BlockArgument locator = BlockArgument.findIn(cc, listVal, 0); + requestedChunks.add(new ChunkPos(locator.block.getPos())); + while (listVal.size() > locator.offset) + { + locator = BlockArgument.findIn(cc, listVal, locator.offset); + requestedChunks.add(new ChunkPos(locator.block.getPos())); + } + } + else + { + BlockArgument locator = BlockArgument.findIn(cc, Collections.singletonList(first), 0); + requestedChunks.add(new ChunkPos(locator.block.getPos())); + } + } + else + { + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + ChunkPos from = new ChunkPos(locator.block.getPos()); + if (lv.size() > 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); + for (int x = Math.min(from.x, to.x); x <= xmax; x++) + { + for (int z = Math.min(from.z, to.z); z <= zmax; z++) + { + requestedChunks.add(new ChunkPos(x, z)); + } + } + } + else + { + requestedChunks.add(from); + } + } + ServerLevel world = cc.level(); + Value[] result = new Value[]{Value.NULL}; + ((CarpetContext) c).server().executeBlocking(() -> + { + Map report = Vanilla.ChunkMap_regenerateChunkRegion(world.getChunkSource().chunkMap, requestedChunks); + result[0] = MapValue.wrap(report.entrySet().stream().collect(Collectors.toMap( + e -> new StringValue(e.getKey()), + e -> new NumericValue(e.getValue()) + ))); + }); + return result[0]; + + */ + }); + + expression.addContextFunction("inhabited_time", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + BlockPos pos = locator.block.getPos(); + return new NumericValue(cc.level().getChunk(pos).getInhabitedTime()); + }); + + expression.addContextFunction("spawn_potential", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + BlockPos pos = locator.block.getPos(); + double requiredCharge = 1; + if (lv.size() > locator.offset) + { + requiredCharge = NumericValue.asNumber(lv.get(locator.offset)).getDouble(); + } + NaturalSpawner.SpawnState charger = cc.level().getChunkSource().getLastSpawnState(); + return charger == null ? Value.NULL : new NumericValue(Vanilla.SpawnState_getPotentialCalculator(charger).getPotentialEnergyChange(pos, requiredCharge) + ); + }); + + expression.addContextFunction("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).getString(); + TicketType 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)).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 == TicketType.PORTAL) // portal + { + cc.level().getChunkSource().addRegionTicket(TicketType.PORTAL, target, radius, pos); + } + else if (ticket == TicketType.POST_TELEPORT) // post teleport + { + cc.level().getChunkSource().addRegionTicket(TicketType.POST_TELEPORT, target, radius, 1); + } + else + { + cc.level().getChunkSource().addRegionTicket(TicketType.UNKNOWN, target, radius, target); + } + return new NumericValue(ticket.timeout()); + }); + + expression.addContextFunction("sample_noise", -1, (c, t, lv) -> + { + CarpetContext cc = (CarpetContext) c; + if (lv.isEmpty()) + { + return ListValue.wrap(cc.registry(Registries.DENSITY_FUNCTION).keySet().stream().map(ValueConversions::of)); + } + ServerLevel level = cc.level(); + BlockArgument locator = BlockArgument.findIn(cc, lv, 0); + BlockPos pos = locator.block.getPos(); + String[] densityFunctionQueries = lv.stream().skip(locator.offset).map(Value::getString).toArray(String[]::new); + if (densityFunctionQueries.length == 0) + { + return ListValue.wrap(cc.registry(Registries.DENSITY_FUNCTION).keySet().stream().map(ValueConversions::of)); + } + NoiseRouter router = level.getChunkSource().randomState().router(); + return densityFunctionQueries.length == 1 + ? NumericValue.of(sampleNoise(router, level, densityFunctionQueries[0], pos)) + : ListValue.wrap(Arrays.stream(densityFunctionQueries).map(s -> NumericValue.of(sampleNoise(router, level, s, pos)))); + }); + } + + public static double sampleNoise(NoiseRouter router, ServerLevel level, String what, BlockPos pos) + { + DensityFunction densityFunction = switch (what) + { + case "barrier_noise" -> router.barrierNoise(); + case "fluid_level_floodedness_noise" -> router.fluidLevelFloodednessNoise(); + case "fluid_level_spread_noise" -> router.fluidLevelSpreadNoise(); + case "lava_noise" -> router.lavaNoise(); + case "temperature" -> router.temperature(); + case "vegetation" -> router.vegetation(); + case "continents" -> router.continents(); + case "erosion" -> router.erosion(); + case "depth" -> router.depth(); + case "ridges" -> router.ridges(); + case "initial_density_without_jaggedness" -> router.initialDensityWithoutJaggedness(); + case "final_density" -> router.finalDensity(); + case "vein_toggle" -> router.veinToggle(); + case "vein_ridged" -> router.veinRidged(); + case "vein_gap" -> router.veinGap(); + default -> stupidWorldgenNoiseCacheGetter.apply(Pair.of(level, what)); + }; + return densityFunction.compute(new DensityFunction.SinglePointContext(pos.getX(), pos.getY(), pos.getZ())); + } + + // to be used with future seedable noise + public static final Function, DensityFunction> stupidWorldgenNoiseCacheGetter = Util.memoize(pair -> { + ServerLevel level = pair.getKey(); + String densityFunctionQuery = pair.getValue(); + ChunkGenerator generator = level.getChunkSource().getGenerator(); + + if (generator instanceof final NoiseBasedChunkGenerator noiseBasedChunkGenerator) + { + Registry densityFunctionRegistry = level.registryAccess().registryOrThrow(Registries.DENSITY_FUNCTION); + NoiseRouter router = noiseBasedChunkGenerator.generatorSettings().value().noiseRouter(); + DensityFunction densityFunction = switch (densityFunctionQuery) + { + case "barrier_noise" -> router.barrierNoise(); + case "fluid_level_floodedness_noise" -> router.fluidLevelFloodednessNoise(); + case "fluid_level_spread_noise" -> router.fluidLevelSpreadNoise(); + case "lava_noise" -> router.lavaNoise(); + case "temperature" -> router.temperature(); + case "vegetation" -> router.vegetation(); + case "continents" -> router.continents(); + case "erosion" -> router.erosion(); + case "depth" -> router.depth(); + case "ridges" -> router.ridges(); + case "initial_density_without_jaggedness" -> router.initialDensityWithoutJaggedness(); + case "final_density" -> router.finalDensity(); + case "vein_toggle" -> router.veinToggle(); + case "vein_ridged" -> router.veinRidged(); + case "vein_gap" -> router.veinGap(); + default -> { + DensityFunction result = densityFunctionRegistry.get(InputValidator.identifierOf(densityFunctionQuery)); + if (result == null) + { + throw new InternalExpressionException("Density function '" + densityFunctionQuery + "' is not defined in the registies."); + } + yield result; + } + }; + + RandomState randomState = RandomState.create( + noiseBasedChunkGenerator.generatorSettings().value(), + level.registryAccess().lookupOrThrow(Registries.NOISE), level.getSeed() + ); + DensityFunction.Visitor visitor = Vanilla.RandomState_getVisitor(randomState); + + return densityFunction.mapAll(visitor); + } + return DensityFunctions.zero(); + }); +} diff --git a/src/main/java/carpet/script/api/package-info.java b/src/main/java/carpet/script/api/package-info.java new file mode 100644 index 0000000..f919f79 --- /dev/null +++ b/src/main/java/carpet/script/api/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.api; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; 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 0000000..e670299 --- /dev/null +++ b/src/main/java/carpet/script/argument/Argument.java @@ -0,0 +1,11 @@ +package carpet.script.argument; + +public abstract class Argument +{ + public final 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 0000000..b9f1040 --- /dev/null +++ b/src/main/java/carpet/script/argument/BlockArgument.java @@ -0,0 +1,129 @@ +package carpet.script.argument; + +import carpet.script.CarpetContext; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.BlockValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import net.minecraft.core.BlockPos; + +import javax.annotation.Nullable; + +public class BlockArgument extends Argument +{ + public final BlockValue block; + @Nullable public final String replacement; + + private BlockArgument(BlockValue b, int o) + { + super(o); + block = b; + replacement = null; + } + + private BlockArgument(BlockValue b, int o, @Nullable String replacement) + { + super(o); + block = b; + this.replacement = replacement; + } + + public static BlockArgument findIn(CarpetContext c, List params, int offset) + { + return findIn(c, params, offset, false, false, false); + } + + public static BlockArgument findIn(CarpetContext c, List params, int offset, boolean acceptString) + { + return findIn(c, params, offset, acceptString, false, false); + } + + public static BlockArgument findIn(CarpetContext c, List params, int offset, boolean acceptString, boolean optional, boolean anyString) + { + return findIn(c, params.listIterator(offset), offset, acceptString, optional, anyString); + } + + public static BlockArgument findIn(CarpetContext c, Iterator params, int offset, boolean acceptString, boolean optional, boolean anyString) + { + try + { + Value v1 = params.next(); + //add conditional from string name + if (optional && v1.isNull()) + { + return new MissingBlockArgument(1 + offset, null); + } + if (anyString && v1 instanceof StringValue) + { + return new MissingBlockArgument(1 + offset, v1.getString()); + } + if (acceptString && v1 instanceof StringValue) + { + return new BlockArgument(BlockValue.fromString(v1.getString(), c.level()), 1 + offset); + } + if (v1 instanceof BlockValue) + { + return new BlockArgument(((BlockValue) v1), 1 + offset); + } + BlockPos pos = c.origin(); + 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( + c.level(), + new BlockPos(pos.getX() + xpos, pos.getY() + ypos, pos.getZ() + zpos) + ), + 1 + offset); + } + int xpos = (int) NumericValue.asNumber(v1).getLong(); + int ypos = (int) NumericValue.asNumber(params.next()).getLong(); + int zpos = (int) NumericValue.asNumber(params.next()).getLong(); + return new BlockArgument( + new BlockValue( + c.level(), + new BlockPos(pos.getX() + xpos, pos.getY() + ypos, pos.getZ() + zpos) + ), + 3 + offset + ); + } + catch (IndexOutOfBoundsException | NoSuchElementException e) + { + throw handleError(optional, acceptString); + } + } + + public static class MissingBlockArgument extends BlockArgument + { + public MissingBlockArgument(int o, @Nullable String replacement) + { + super(BlockValue.NONE, o, replacement); + } + } + + 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/FileArgument.java b/src/main/java/carpet/script/argument/FileArgument.java new file mode 100644 index 0000000..f4853a5 --- /dev/null +++ b/src/main/java/carpet/script/argument/FileArgument.java @@ -0,0 +1,664 @@ +package carpet.script.argument; + +import carpet.script.CarpetScriptServer; +import carpet.script.Context; +import carpet.script.Module; +import carpet.script.ScriptHost; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.value.MapValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import net.minecraft.ReportedException; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.TagTypes; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.tuple.Pair; + +import javax.annotation.Nullable; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class FileArgument +{ + public final String resource; + public final Type type; + public final String zipContainer; + public final boolean isFolder; + public final boolean isShared; + public final Reason reason; + private FileSystem zfs; + private Path zipPath; + private final ScriptHost host; + + public static final Object writeIOSync = new Object(); + + public void close() + { + if (zfs != null && zfs.isOpen()) + { + try + { + zfs.close(); + } + catch (IOException e) + { + throw new InternalExpressionException("Unable to close zip container: " + zipContainer); + } + zfs = null; + } + } + + public enum Type + { + RAW("raw", ".txt"), + TEXT("text", ".txt"), + NBT("nbt", ".nbt"), + JSON("json", ".json"), + FOLDER("folder", ""), + ANY("any", ""); + + private final String id; + private final String extension; + + private static final Map of = Arrays.stream(values()).collect(Collectors.toMap(t -> t.id, t -> t)); + + Type(String id, String extension) + { + this.id = id; + this.extension = extension; + } + } + + public enum Reason + { + READ, CREATE, DELETE + } + + public FileArgument(@Nullable String resource, Type type, @Nullable String zipContainer, boolean isFolder, boolean isShared, Reason reason, ScriptHost host) + { + this.resource = resource; + this.type = type; + this.zipContainer = zipContainer; + this.isFolder = isFolder; + this.isShared = isShared; + this.reason = reason; + this.zfs = null; + this.zipPath = null; + this.host = host; + } + + @Override + public String toString() + { + return "path: " + resource + " zip: " + zipContainer + " type: " + type.id + " folder: " + isFolder + " shared: " + isShared + " reason: " + reason; + } + + public static FileArgument from(Context context, List lv, boolean isFolder, Reason reason) + { + if (lv.size() < 2) + { + throw new InternalExpressionException("File functions require path and type as first two arguments"); + } + String origtype = lv.get(1).getString().toLowerCase(Locale.ROOT); + boolean shared = origtype.startsWith("shared_"); + String typeString = shared ? origtype.substring(7) : origtype; //len(shared_) + Type type = Type.of.get(typeString); + if (type == null) + { + throw new InternalExpressionException("Unsupported file type: " + origtype); + } + Pair resource = recognizeResource(lv.get(0).getString(), isFolder, type); + if (type == Type.FOLDER && !isFolder) + { + throw new InternalExpressionException("Folder types are no supported for this IO function"); + } + return new FileArgument(resource.getLeft(), type, resource.getRight(), isFolder, shared, reason, context.host); + + } + + public static FileArgument resourceFromPath(ScriptHost host, String path, Reason reason, boolean shared) + { + Pair resource = recognizeResource(path, false, Type.ANY); + return new FileArgument(resource.getLeft(), Type.ANY, resource.getRight(), false, shared, reason, host); + } + + public static Pair recognizeResource(String origfile, boolean isFolder, Type type) + { + String[] pathElements = origfile.split("[/\\\\]+"); + List path = new ArrayList<>(); + String zipPath = null; + for (int i = 0; i < pathElements.length; i++) + { + String token = pathElements[i]; + boolean isZip = token.endsWith(".zip") && (isFolder || (i < pathElements.length - 1)); + if (zipPath != null && isZip) + { + throw new InternalExpressionException(token + " indicates zip access in an already zipped location " + zipPath); + } + if (isZip) + { + token = token.substring(0, token.length() - 4); + } + token = (type == Type.ANY && i == pathElements.length - 1) ? // sloppy really, but should work + token.replaceAll("[^A-Za-z0-9\\-+_.]", "") : + token.replaceAll("[^A-Za-z0-9\\-+_]", ""); + if (token.isEmpty()) + { + continue; + } + if (isZip) + { + token = token + ".zip"; + } + path.add(token); + if (isZip) + { + zipPath = String.join("/", path); + path.clear(); + } + } + if (path.isEmpty() && !isFolder) + { + throw new InternalExpressionException( + "Cannot use " + origfile + " as resource name: indicated path is empty" + ((zipPath == null) ? "" : " in zip container " + zipPath) + ); + } + return Pair.of(String.join("/", path), zipPath); + } + + private Path resolve(String suffix) + { + return host.resolveScriptFile(suffix); + } + + @Nullable + private Path toPath(@Nullable Module module) + { + if (!isShared && module == null) + { + return null; + } + if (zipContainer == null) + { + return resolve(getDescriptor(module, resource) + (isFolder ? "" : type.extension)); + } + else + { + if (zfs == null) + { + Map env = new HashMap<>(); + if (reason == Reason.CREATE) + { + env.put("create", "true"); + } + zipPath = resolve(getDescriptor(module, zipContainer)); + if (!Files.exists(zipPath) && reason != Reason.CREATE) + { + return null; // no zip file + } + try + { + if (!Files.exists(zipPath.getParent())) + { + Files.createDirectories(zipPath.getParent()); + } + zfs = FileSystems.newFileSystem(URI.create("jar:" + zipPath.toUri()), env); + } + catch (FileSystemNotFoundException | IOException e) + { + CarpetScriptServer.LOG.warn("Exception when opening zip file", e); + throw new ThrowStatement("Unable to open zip file: " + zipContainer, Throwables.IO_EXCEPTION); + } + } + return zfs.getPath(resource + (isFolder ? "/" : type.extension)); + } + } + + @Nullable + private Path moduleRootPath(@Nullable Module module) + { + return !isShared && module == null + ? null + : resolve(isShared ? "shared" : module.name() + ".data"); + } + + public String getDisplayPath() + { + return (isShared ? "shared/" : "") + (zipContainer != null ? zipContainer + "/" : "") + resource + type.extension; + } + + private String getDescriptor(@Nullable Module module, @Nullable String res) + { + if (isShared) + { + return res.isEmpty() ? "shared" : "shared/" + res; + } + if (module != null) // appdata + { + return module.name() + ".data" + (res == null || res.isEmpty() ? "" : "/" + res); + } + throw new InternalExpressionException("Invalid file descriptor: " + res); + } + + + public boolean findPathAndApply(Module module, Consumer action) + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module);//, resourceName, supportedTypes.get(type), isShared); + if (dataFile == null) + { + return false; + } + createPaths(dataFile); + action.accept(dataFile); + } + } + finally + { + close(); + } + return true; + } + + @Nullable + public Stream listFiles(Module module) + { + Path dir = toPath(module); + if (dir == null || !Files.exists(dir)) + { + return null; + } + String ext = type.extension; + try + { + return Files.list(dir).filter(path -> (type == Type.FOLDER) + ? Files.isDirectory(path) + : (Files.isRegularFile(path) && path.toString().endsWith(ext)) + ); + } + catch (IOException ignored) + { + return null; + } + } + + @Nullable + public Stream listFolder(Module module) + { + Stream strings; + try (Stream result = listFiles(module)) + { + synchronized (writeIOSync) + { + if (result == null) + { + return null; + } + Path rootPath = moduleRootPath(module); + if (rootPath == null) + { + return null; + } + String zipComponent = (zipContainer != null) ? rootPath.relativize(zipPath).toString() : null; + // need to evaluate the stream before exiting try-with-resources else there'll be no data to stream + strings = (zipContainer == null) + ? result.map(p -> rootPath.relativize(p).toString().replaceAll("[\\\\/]+", "/")).toList().stream() + : result.map(p -> (zipComponent + '/' + p.toString()).replaceAll("[\\\\/]+", "/")).toList().stream(); + } + } + finally + { + close(); + } + // java 8 paths are inconsistent. in java 16 they all should not have trailing slashes + return type == Type.FOLDER + ? strings.map(s -> s.endsWith("/") ? s.substring(0, s.length() - 1) : s) + : strings.map(FilenameUtils::removeExtension); + } + + private void createPaths(Path file) + { + try + { + if ((zipContainer == null || file.getParent() != null) && + !Files.exists(file.getParent()) && + Files.createDirectories(file.getParent()) == null + ) + { + throw new IOException(); + } + } + catch (IOException e) + { + CarpetScriptServer.LOG.warn("IOException when creating paths", e); + throw new ThrowStatement("Unable to create paths for " + file, Throwables.IO_EXCEPTION); + } + } + + public boolean appendToTextFile(Module module, List message) + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module); + if (dataFile == null) + { + return false; + } + createPaths(dataFile); + OutputStream out = Files.newOutputStream(dataFile, StandardOpenOption.APPEND, StandardOpenOption.CREATE); + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) + { + for (String line : message) + { + writer.append(line); + if (type == Type.TEXT) + { + writer.newLine(); + } + } + } + } + } + catch (IOException e) + { + CarpetScriptServer.LOG.warn("IOException when appending to text file", e); + throw new ThrowStatement("Error when writing to the file: " + e, Throwables.IO_EXCEPTION); + } + finally + { + close(); + } + return true; + } + + @Nullable + public Tag getNbtData(Module module) // aka getData + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module); + if (dataFile == null || !Files.exists(dataFile)) + { + return null; + } + return readTag(dataFile); + } + } + finally + { + close(); + } + } + + //copied private method from net.minecraft.nbt.NbtIo.read() + // to read non-compound tags - these won't be compressed + @Nullable + public static Tag readTag(Path path) + { + try + { + return NbtIo.readCompressed(Files.newInputStream(path), NbtAccounter.unlimitedHeap()); + } + catch (IOException e) + { + // Copy of NbtIo.read(File) because that's now client-side only + if (!Files.exists(path)) + { + return null; + } + try (DataInputStream in = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)))) + { + return NbtIo.read(in); + } + catch (IOException ioException) + { + // not compressed compound tag neither uncompressed compound tag - trying any type of a tag + try (DataInputStream dataInputStream = new DataInputStream(new BufferedInputStream(Files.newInputStream(path)))) + { + byte b = dataInputStream.readByte(); + if (b == 0) + { + return null; + } + else + { + dataInputStream.readUTF(); + return TagTypes.getType(b).load(dataInputStream, NbtAccounter.unlimitedHeap()); + } + } + catch (IOException secondIO) + { + CarpetScriptServer.LOG.warn("IOException when trying to read nbt file, something may have gone wrong with the fs", e); + CarpetScriptServer.LOG.warn("", ioException); + CarpetScriptServer.LOG.warn("", secondIO); + throw new ThrowStatement("Not a valid NBT tag in " + path, Throwables.NBT_ERROR); + } + } + } + catch (ReportedException e) + { + throw new ThrowStatement("Error when reading NBT file " + path, Throwables.NBT_ERROR); + } + } + + public boolean saveNbtData(Module module, Tag tag) // aka saveData + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module); + if (dataFile == null) + { + return false; + } + createPaths(dataFile); + return writeTagDisk(tag, dataFile, zipContainer != null); + } + } + finally + { + close(); + } + } + + //copied private method from net.minecraft.nbt.NbtIo.write() and client method safe_write + public static boolean writeTagDisk(Tag tag, Path path, boolean zipped) + { + Path original = path; + try + { + if (!zipped) + { + path = path.getParent().resolve(path.getFileName() + "_tmp"); + Files.deleteIfExists(path); + } + + if (tag instanceof final CompoundTag cTag) + { + NbtIo.writeCompressed(cTag, Files.newOutputStream(path)); + } + else + { + try (DataOutputStream dataOutputStream = new DataOutputStream(Files.newOutputStream(path))) + { + dataOutputStream.writeByte(tag.getId()); + if (tag.getId() != 0) + { + dataOutputStream.writeUTF(""); + tag.write(dataOutputStream); + } + } + } + if (!zipped) + { + Files.deleteIfExists(original); + Files.move(path, original); + } + return true; + } + catch (IOException e) + { + CarpetScriptServer.LOG.warn("IO Exception when writing nbt file", e); + throw new ThrowStatement("Unable to write tag to " + original, Throwables.IO_EXCEPTION); + } + } + + public boolean dropExistingFile(Module module) + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module); + if (dataFile == null) + { + return false; + } + return Files.deleteIfExists(dataFile); + } + } + catch (IOException e) + { + CarpetScriptServer.LOG.warn("IOException when removing file", e); + throw new ThrowStatement("Error while removing file: " + getDisplayPath(), Throwables.IO_EXCEPTION); + } + finally + { + close(); + } + } + + @Nullable + public List listFile(Module module) + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module); + if (dataFile == null) + { + return null; + } + if (!Files.exists(dataFile)) + { + return null; + } + return listFileContent(dataFile); + } + } + finally + { + close(); + } + } + + public static List listFileContent(Path filePath) + { + try (BufferedReader reader = Files.newBufferedReader(filePath, 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) + { + CarpetScriptServer.LOG.warn("IOException when reading text file", e); + throw new ThrowStatement("Failed to read text file " + filePath, Throwables.IO_EXCEPTION); + } + } + + @Nullable + public JsonElement readJsonFile(Module module) + { + try + { + synchronized (writeIOSync) + { + Path dataFile = toPath(module); + if (dataFile == null || !Files.exists(dataFile)) + { + return null; + } + return readJsonContent(dataFile); + } + } + finally + { + close(); + } + } + + public static JsonElement readJsonContent(Path filePath) + { + try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) + { + return JsonParser.parseReader(reader); + } + catch (JsonParseException e) + { + Throwable exc = e; + if (e.getCause() != null) + { + exc = e.getCause(); + } + throw new ThrowStatement(MapValue.wrap(Map.of( + StringValue.of("error"), StringValue.of(exc.getMessage()), + StringValue.of("path"), StringValue.of(filePath.toString()) + )), Throwables.JSON_ERROR); + } + catch (IOException e) + { + CarpetScriptServer.LOG.warn("IOException when reading JSON file", e); + throw new ThrowStatement("Failed to read json file content " + filePath, Throwables.IO_EXCEPTION); + } + } + +} 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 0000000..b30f2f5 --- /dev/null +++ b/src/main/java/carpet/script/argument/FunctionArgument.java @@ -0,0 +1,124 @@ +package carpet.script.argument; + +import carpet.script.Context; +import carpet.script.ScriptHost; +import carpet.script.Module; +import carpet.script.command.CommandArgument; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.FunctionValue; +import carpet.script.value.ListValue; +import carpet.script.value.Value; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class FunctionArgument extends Argument +{ + public final FunctionValue function; + public final List args; + + private FunctionArgument(@Nullable FunctionValue function, int offset, List args) + { + super(offset); + this.function = function; + this.args = args; + } + + /** + * @param c context + * @param module module + * @param params list of params + * @param offset offset where to start looking for functional argument + * @param allowNone none indicates no function present, otherwise it will croak + * @param checkArgs whether the caller expects trailing parameters to fully resolve function argument list + * if not - argument count check will not be performed and its up to the caller to verify + * if the number of supplied arguments is right + * @return argument data + */ + public static FunctionArgument findIn( + Context c, + Module module, + List params, + int offset, + boolean allowNone, + boolean checkArgs) + { + Value functionValue = params.get(offset); + if (functionValue.isNull()) + { + if (allowNone) + { + return new FunctionArgument(null, offset + 1, Collections.emptyList()); + } + throw new InternalExpressionException("function argument cannot be null"); + } + if (!(functionValue instanceof FunctionValue)) + { + String name = functionValue.getString(); + functionValue = c.host.getAssertFunction(module, name); + } + FunctionValue fun = (FunctionValue) functionValue; + int argsize = fun.getArguments().size(); + if (checkArgs) + { + int extraargs = params.size() - argsize - offset - 1; + if (extraargs < 0) + { + throw new InternalExpressionException("Function " + fun.getPrettyString() + " requires at least " + fun.getArguments().size() + " arguments"); + } + if (extraargs > 0 && fun.getVarArgs() == null) + { + throw new InternalExpressionException("Function " + fun.getPrettyString() + " requires " + fun.getArguments().size() + " arguments"); + } + } + List lvargs = new ArrayList<>(); + for (int i = offset + 1, mx = params.size(); i < mx; i++) + { + lvargs.add(params.get(i)); + } + return new FunctionArgument(fun, offset + 1 + argsize, lvargs); + } + + public static FunctionArgument fromCommandSpec(ScriptHost host, Value funSpec) throws CommandSyntaxException + { + FunctionValue function; + List args = Collections.emptyList(); + if (!(funSpec instanceof ListValue)) + { + funSpec = ListValue.of(funSpec); + } + List params = ((ListValue) funSpec).getItems(); + if (params.isEmpty()) + { + throw CommandArgument.error("Function has empty spec"); + } + Value first = params.get(0); + if (first instanceof FunctionValue) + { + function = (FunctionValue) first; + } + else + { + String name = first.getString(); + function = host.getFunction(name); + if (function == null) + { + throw CommandArgument.error("Function " + name + " is not defined yet"); + } + } + if (params.size() > 1) + { + args = params.subList(1, params.size()); + } + return new FunctionArgument(function, 0, args); + } + + public List checkedArgs() + { + function.checkArgs(args.size()); + return args; + } +} 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 0000000..6f475f4 --- /dev/null +++ b/src/main/java/carpet/script/argument/Vector3Argument.java @@ -0,0 +1,117 @@ +package carpet.script.argument; + +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.BlockValue; +import carpet.script.value.EntityValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; + +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +public class Vector3Argument extends Argument +{ + public Vec3 vec; + public final double yaw; + public final double pitch; + public boolean fromBlock = false; + @Nullable + public Entity entity = null; + + private Vector3Argument(Vec3 v, int o) + { + super(o); + this.vec = v; + this.yaw = 0.0D; + this.pitch = 0.0D; + } + + private Vector3Argument(Vec3 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; + } + + private Vector3Argument withEntity(Entity e) + { + entity = e; + return this; + } + + public static Vector3Argument findIn(List params, int offset) + { + return findIn(params, offset, false, false); + } + + public static Vector3Argument findIn(List params, int offset, boolean optionalDirection, boolean optionalEntity) + { + return findIn(params.listIterator(offset), offset, optionalDirection, optionalEntity); + } + + public static Vector3Argument findIn(Iterator params, int offset, boolean optionalDirection, boolean optionalEntity) + { + try + { + Value v1 = params.next(); + if (v1 instanceof BlockValue) + { + return (new Vector3Argument(Vec3.atCenterOf(((BlockValue) v1).getPos()), 1 + offset)).fromBlock(); + } + if (optionalEntity && v1 instanceof EntityValue) + { + Entity e = ((EntityValue) v1).getEntity(); + return new Vector3Argument(e.position(), 1 + offset).withEntity(e); + } + if (v1 instanceof ListValue) + { + List args = ((ListValue) v1).getItems(); + Vec3 pos = new Vec3( + 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); + } + Vec3 pos = new Vec3( + NumericValue.asNumber(v1).getDouble(), + NumericValue.asNumber(params.next()).getDouble(), + NumericValue.asNumber(params.next()).getDouble()); + double yaw = 0.0D; + double pitch = 0.0D; + int eatenLength = 3; + if (params.hasNext() && optionalDirection) + { + yaw = NumericValue.asNumber(params.next()).getDouble(); + pitch = NumericValue.asNumber(params.next()).getDouble(); + eatenLength = 5; + } + + return new Vector3Argument(pos, offset + eatenLength, yaw, pitch); + } + catch (IndexOutOfBoundsException | NoSuchElementException 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/argument/package-info.java b/src/main/java/carpet/script/argument/package-info.java new file mode 100644 index 0000000..88201df --- /dev/null +++ b/src/main/java/carpet/script/argument/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.argument; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/command/CommandArgument.java b/src/main/java/carpet/script/command/CommandArgument.java new file mode 100644 index 0000000..5d0fbab --- /dev/null +++ b/src/main/java/carpet/script/command/CommandArgument.java @@ -0,0 +1,1307 @@ +package carpet.script.command; + +import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; +import carpet.script.argument.FunctionArgument; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.value.BlockValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import com.mojang.datafixers.util.Either; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; + +import com.google.common.collect.Lists; +import com.mojang.authlib.GameProfile; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import net.minecraft.ChatFormatting; +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.AngleArgument; +import net.minecraft.commands.arguments.ColorArgument; +import net.minecraft.commands.arguments.CompoundTagArgument; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.commands.arguments.MessageArgument; +import net.minecraft.commands.arguments.NbtPathArgument; +import net.minecraft.commands.arguments.NbtTagArgument; +import net.minecraft.commands.arguments.ObjectiveArgument; +import net.minecraft.commands.arguments.ObjectiveCriteriaArgument; +import net.minecraft.commands.arguments.ParticleArgument; +import net.minecraft.commands.arguments.RangeArgument; +import net.minecraft.commands.arguments.ResourceArgument; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.commands.arguments.ResourceOrTagArgument; +import net.minecraft.commands.arguments.ScoreHolderArgument; +import net.minecraft.commands.arguments.ScoreboardSlotArgument; +import net.minecraft.commands.arguments.TeamArgument; +import net.minecraft.commands.arguments.TimeArgument; +import net.minecraft.commands.arguments.UuidArgument; +import net.minecraft.commands.arguments.blocks.BlockInput; +import net.minecraft.commands.arguments.blocks.BlockPredicateArgument; +import net.minecraft.commands.arguments.blocks.BlockStateArgument; +import net.minecraft.commands.arguments.coordinates.ColumnPosArgument; +import net.minecraft.commands.arguments.coordinates.RotationArgument; +import net.minecraft.commands.arguments.coordinates.SwizzleArgument; +import net.minecraft.commands.arguments.coordinates.Vec2Argument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.commands.arguments.item.ItemArgument; +import net.minecraft.commands.synchronization.SuggestionProviders; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.commands.BossBarCommands; +import net.minecraft.server.commands.LootCommand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.scores.ScoreHolder; + +import javax.annotation.Nullable; + +import static net.minecraft.commands.Commands.argument; + +public abstract class CommandArgument +{ + public static CommandSyntaxException error(String text) + { + return new SimpleCommandExceptionType(Component.literal(text)).create(); + } + + private static final List baseTypes = Lists.newArrayList( + // default + new StringArgument(), + // vanilla arguments as per https://minecraft.wiki/w/Argument_types + new VanillaUnconfigurableArgument("bool", BoolArgumentType::bool, + (c, p) -> BooleanValue.of(BoolArgumentType.getBool(c, p)), false + ), + new FloatArgument(), + new IntArgument(), + new WordArgument(), new GreedyStringArgument(), + new VanillaUnconfigurableArgument("yaw", AngleArgument::angle, // angle + (c, p) -> new NumericValue(AngleArgument.getAngle(c, p)), true + ), + new BlockPosArgument(), + new VanillaUnconfigurableArgument("block", BlockStateArgument::block, + (c, p) -> { + BlockInput result = BlockStateArgument.getBlock(c, p); + return new BlockValue(result.getState(), c.getSource().getLevel(), Vanilla.BlockInput_getTag(result)); + }, + param -> (ctx, builder) -> ctx.getArgument(param, BlockStateArgument.class).listSuggestions(ctx, builder) + ), + new VanillaUnconfigurableArgument("blockpredicate", BlockPredicateArgument::blockPredicate, + (c, p) -> ValueConversions.ofBlockPredicate(c.getSource().getServer().registryAccess(), BlockPredicateArgument.getBlockPredicate(c, p)), + param -> (ctx, builder) -> ctx.getArgument(param, BlockPredicateArgument.class).listSuggestions(ctx, builder) + ), + new VanillaUnconfigurableArgument("teamcolor", ColorArgument::color, + (c, p) -> { + ChatFormatting format = ColorArgument.getColor(c, p); + return ListValue.of(StringValue.of(format.getName()), ValueConversions.ofRGB(format.getColor())); + }, + false + ), + new VanillaUnconfigurableArgument("columnpos", ColumnPosArgument::columnPos, + (c, p) -> ValueConversions.of(ColumnPosArgument.getColumnPos(c, p)), false + ), + // component // raw json + new VanillaUnconfigurableArgument("dimension", DimensionArgument::dimension, + (c, p) -> ValueConversions.of(DimensionArgument.getDimension(c, p)), false + ), + new EntityArgument(), + new VanillaUnconfigurableArgument("anchor", EntityAnchorArgument::anchor, + (c, p) -> StringValue.of(EntityAnchorArgument.getAnchor(c, p).name()), false + ), + new VanillaUnconfigurableArgument("entitytype", c -> ResourceArgument.resource(c, Registries.ENTITY_TYPE), + (c, p) -> ValueConversions.of(ResourceArgument.getSummonableEntityType(c, p).key()), SuggestionProviders.SUMMONABLE_ENTITIES + ), + new VanillaUnconfigurableArgument("floatrange", RangeArgument::floatRange, + (c, p) -> ValueConversions.of(c.getArgument(p, MinMaxBounds.Doubles.class)), true + ), + // function?? + + new PlayerProfileArgument(), + new VanillaUnconfigurableArgument("intrange", RangeArgument::intRange, + (c, p) -> ValueConversions.of(RangeArgument.Ints.getRange(c, p)), true + ), + new VanillaUnconfigurableArgument("enchantment", Registries.ENCHANTMENT), + + // item_predicate ?? //same as item but accepts tags, not sure right now + new SlotArgument(), + new VanillaUnconfigurableArgument("item", ItemArgument::item, + (c, p) -> ValueConversions.of(ItemArgument.getItem(c, p).createItemStack(1, false), c.getSource().registryAccess()), + param -> (ctx, builder) -> ctx.getArgument(param, ItemArgument.class).listSuggestions(ctx, builder) + ), + new VanillaUnconfigurableArgument("message", MessageArgument::message, + (c, p) -> new FormattedTextValue(MessageArgument.getMessage(c, p)), true + ), + new VanillaUnconfigurableArgument("effect", Registries.MOB_EFFECT), + + new TagArgument(), // for nbt_compound_tag and nbt_tag + new VanillaUnconfigurableArgument("path", NbtPathArgument::nbtPath, + (c, p) -> StringValue.of(NbtPathArgument.getPath(c, p).toString()), true + ), + new VanillaUnconfigurableArgument("objective", ObjectiveArgument::objective, + (c, p) -> ValueConversions.of(ObjectiveArgument.getObjective(c, p)), false + ), + new VanillaUnconfigurableArgument("criterion", ObjectiveCriteriaArgument::criteria, + (c, p) -> StringValue.of(ObjectiveCriteriaArgument.getCriteria(c, p).getName()), false + ), + // operation // not sure if we need it, you have scarpet for that + new VanillaUnconfigurableArgument("particle", ParticleArgument::particle, + (c, p) -> ValueConversions.of(ParticleArgument.getParticle(c, p), c.getSource().registryAccess()), (c, b) -> SharedSuggestionProvider.suggestResource(c.getSource().getServer().registryAccess().registryOrThrow(Registries.PARTICLE_TYPE).keySet(), b) + ), + + // resource / identifier section + + new VanillaUnconfigurableArgument("recipe", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getRecipe(c, p).id()), SuggestionProviders.ALL_RECIPES + ), + new VanillaUnconfigurableArgument("advancement", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getAdvancement(c, p).id()), (ctx, builder) -> SharedSuggestionProvider.suggestResource(ctx.getSource().getServer().getAdvancements().getAllAdvancements().stream().map(AdvancementHolder::id), builder) + ), + new VanillaUnconfigurableArgument("lootcondition", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getId(c, p)), (ctx, builder) -> SharedSuggestionProvider.suggestResource(ctx.getSource().getServer().reloadableRegistries().getKeys(Registries.LOOT_CONDITION_TYPE), builder) + ), + new VanillaUnconfigurableArgument("loottable", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getId(c, p)), LootCommand.SUGGEST_LOOT_TABLE + ), + new VanillaUnconfigurableArgument("attribute", Registries.ATTRIBUTE), + + new VanillaUnconfigurableArgument("boss", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getId(c, p)), BossBarCommands.SUGGEST_BOSS_BAR + ), + + new VanillaUnconfigurableArgument("biome", c -> ResourceOrTagArgument.resourceOrTag(c, Registries.BIOME), + (c, p) -> { + ResourceOrTagArgument.Result result = ResourceOrTagArgument.getResourceOrTag(c, "biome", Registries.BIOME); + Either, HolderSet.Named> res = result.unwrap(); + if (res.left().isPresent()) + { + return ValueConversions.of(res.left().get().key()); + } + if (res.right().isPresent()) + { + return ValueConversions.of(res.right().get().key()); + } + return Value.NULL; + }, (ctx, builder) -> SharedSuggestionProvider.suggestResource(ctx.getSource().getServer().registryAccess().registryOrThrow(Registries.BIOME).keySet(), builder) + ), + new VanillaUnconfigurableArgument("sound", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getId(c, p)), SuggestionProviders.AVAILABLE_SOUNDS + ), + new VanillaUnconfigurableArgument("storekey", ResourceLocationArgument::id, + (c, p) -> ValueConversions.of(ResourceLocationArgument.getId(c, p)), (ctx, builder) -> SharedSuggestionProvider.suggestResource(ctx.getSource().getServer().getCommandStorage().keys(), builder) + ), + + // default + new CustomIdentifierArgument(), + + // end resource / identifier // I would be great if you guys have suggestions for that. + + new VanillaUnconfigurableArgument("rotation", + RotationArgument::rotation, + (c, p) -> { + Vec2 rot = RotationArgument.getRotation(c, p).getRotation(c.getSource()); + return ListValue.of(new NumericValue(rot.x), new NumericValue(rot.y)); + }, + true + ), + new ScoreholderArgument(), + new VanillaUnconfigurableArgument("scoreboardslot", ScoreboardSlotArgument::displaySlot, + (c, p) -> StringValue.of(ScoreboardSlotArgument.getDisplaySlot(c, p).getSerializedName()), false + ), + new VanillaUnconfigurableArgument("swizzle", SwizzleArgument::swizzle, + (c, p) -> StringValue.of(SwizzleArgument.getSwizzle(c, p).stream().map(Direction.Axis::getSerializedName).collect(Collectors.joining())), true + ), + new VanillaUnconfigurableArgument("team", TeamArgument::team, + (c, p) -> StringValue.of(TeamArgument.getTeam(c, p).getName()), false + ), + new VanillaUnconfigurableArgument("time", TimeArgument::time, + (c, p) -> new NumericValue(IntegerArgumentType.getInteger(c, p)), false + ), + new VanillaUnconfigurableArgument("uuid", UuidArgument::uuid, + (c, p) -> StringValue.of(UuidArgument.getUuid(c, p).toString()), false + ), + new VanillaUnconfigurableArgument("surfacelocation", Vec2Argument::vec2, + (c, p) -> { + Vec2 res = Vec2Argument.getVec2(c, p); + return ListValue.of(NumericValue.of(res.x), NumericValue.of(res.y)); + }, + false + ), + new LocationArgument() + ); + + public static final Map builtIns = baseTypes.stream().collect(Collectors.toMap(CommandArgument::getTypeSuffix, a -> a)); + + public static final CommandArgument DEFAULT = baseTypes.get(0); + + public static CommandArgument getTypeForArgument(String argument, CarpetScriptHost host) + { + String[] components = argument.split("_"); + CommandArgument arg; + for (int i = 0; i < components.length; i++) + { + String candidate = String.join("_", Arrays.asList(components).subList(i, components.length)); + arg = host.appArgTypes.get(candidate); + if (arg != null) + { + return arg; + } + arg = builtIns.get(candidate); + if (arg != null) + { + return arg; + } + } + return DEFAULT; + } + + public static RequiredArgumentBuilder argumentNode(String param, CarpetScriptHost host) throws CommandSyntaxException + { + CommandArgument arg = getTypeForArgument(param, host); + if (arg.suggestionProvider != null) + { + return argument(param, arg.getArgumentType(host)).suggests(arg.suggestionProvider.apply(param)); + } + if (!arg.needsMatching) + { + return argument(param, arg.getArgumentType(host)); + } + String hostName = host.getName(); + CarpetScriptServer scriptServer = host.scriptServer(); + return argument(param, arg.getArgumentType(host)).suggests((ctx, b) -> { + CarpetScriptHost cHost = scriptServer.modules.get(hostName).retrieveOwnForExecution(ctx.getSource()); + return arg.suggest(ctx, b, cHost); + }); + } + + protected String suffix; + @Nullable + protected Collection examples; + protected boolean needsMatching; + protected boolean caseSensitive = true; + protected Function> suggestionProvider; + protected FunctionArgument customSuggester; + + + protected CommandArgument( + String suffix, + @Nullable Collection examples, + boolean suggestFromExamples) + { + this.suffix = suffix; + this.examples = examples; + this.needsMatching = suggestFromExamples; + } + + protected abstract ArgumentType getArgumentType(CarpetScriptHost host) throws CommandSyntaxException; + + + public static Value getValue(CommandContext context, String param, CarpetScriptHost host) throws CommandSyntaxException + { + return getTypeForArgument(param, host).getValueFromContext(context, param); + } + + protected abstract Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException; + + public String getTypeSuffix() + { + return suffix; + } + + public static CommandArgument buildFromConfig(String suffix, Map config, CarpetScriptHost host) throws CommandSyntaxException + { + if (!config.containsKey("type")) + { + throw CommandArgument.error("Custom type " + suffix + " should at least specify the type"); + } + String baseType = config.get("type").getString(); + if (!builtIns.containsKey(baseType)) + { + throw CommandArgument.error("Unknown base type " + baseType + " for custom type " + suffix); + } + CommandArgument variant = builtIns.get(baseType).factory(host.scriptServer().server).get(); + variant.suffix = suffix; + variant.configure(config, host); + return variant; + } + + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + caseSensitive = config.getOrDefault("case_sensitive", Value.TRUE).getBoolean(); + if (config.containsKey("suggester")) + { + customSuggester = FunctionArgument.fromCommandSpec(host, config.get("suggester")); + } + if (config.containsKey("suggest")) + { + if (config.containsKey("suggester")) + { + throw error("Attempted to provide 'suggest' list while 'suggester' is present" + " for custom type " + suffix); + } + Value suggestionValue = config.get("suggest"); + if (!(suggestionValue instanceof ListValue)) + { + throw error("Argument suggestions needs to be a list" + " for custom type " + suffix); + } + examples = ((ListValue) suggestionValue).getItems().stream() + .map(Value::getString) + .collect(Collectors.toSet()); + if (!examples.isEmpty()) + { + needsMatching = true; + } + } + } + + public CompletableFuture suggest( + CommandContext context, + SuggestionsBuilder suggestionsBuilder, + CarpetScriptHost host + ) throws CommandSyntaxException + { + String prefix = suggestionsBuilder.getRemaining(); + if (!caseSensitive) + { + prefix = prefix.toLowerCase(Locale.ROOT); + } + suggestFor(context, prefix, host).forEach(suggestionsBuilder::suggest); + return suggestionsBuilder.buildFuture(); + } + + protected List suggestFor(CommandContext context, String prefix, CarpetScriptHost host) throws CommandSyntaxException + { + return getOptions(context, host).stream().filter(s -> optionMatchesPrefix(prefix, s)).collect(Collectors.toList()); + } + + protected Collection getOptions(CommandContext context, CarpetScriptHost host) throws CommandSyntaxException + { + if (customSuggester != null) + { + Runnable currentSection = Carpet.startProfilerSection("Scarpet command"); + Map params = new HashMap<>(); + for (ParsedCommandNode pnode : context.getNodes()) + { + CommandNode node = pnode.getNode(); + if (node instanceof ArgumentCommandNode) + { + params.put(StringValue.of(node.getName()), CommandArgument.getValue(context, node.getName(), host)); + } + } + List args = new ArrayList<>(customSuggester.args.size() + 1); + args.add(MapValue.wrap(params)); + args.addAll(customSuggester.args); + Value response = host.handleCommand(context.getSource(), customSuggester.function, args); + if (!(response instanceof ListValue)) + { + throw error("Custom suggester should return a list of options" + " for custom type " + suffix); + } + Collection res = ((ListValue) response).getItems().stream().map(Value::getString).collect(Collectors.toList()); + currentSection.run(); + return res; + } + return needsMatching ? examples : Collections.singletonList("... " + getTypeSuffix()); + } + + protected boolean optionMatchesPrefix(String prefix, String option) + { + if (!caseSensitive) + { + option = option.toLowerCase(Locale.ROOT); + } + for (int i = 0; !option.startsWith(prefix, i); ++i) + { + i = option.indexOf('_', i); + if (i < 0) + { + return false; + } + } + return true; + } + + protected abstract Supplier factory(MinecraftServer server); + + private static class StringArgument extends CommandArgument + { + Set validOptions = Collections.emptySet(); + + private StringArgument() + { + super("string", StringArgumentType.StringType.QUOTABLE_PHRASE.getExamples(), true); + } + + @Override + public ArgumentType getArgumentType(CarpetScriptHost host) + { + return StringArgumentType.string(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + String choseValue = StringArgumentType.getString(context, param); + if (!caseSensitive) + { + choseValue = choseValue.toLowerCase(Locale.ROOT); + } + if (!validOptions.isEmpty() && !validOptions.contains(choseValue)) + { + throw new SimpleCommandExceptionType(Component.literal("Incorrect value for " + param + ": " + choseValue + " for custom type " + suffix)).create(); + } + return StringValue.of(choseValue); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + if (config.containsKey("options")) + { + Value optionsValue = config.get("options"); + if (!(optionsValue instanceof ListValue)) + { + throw error("Custom string type requires options passed as a list" + " for custom type " + suffix); + } + validOptions = ((ListValue) optionsValue).getItems().stream() + .map(v -> caseSensitive ? v.getString() : (v.getString().toLowerCase(Locale.ROOT))) + .collect(Collectors.toSet()); + } + } + + @Override + protected Collection getOptions(CommandContext context, CarpetScriptHost host) throws CommandSyntaxException + { + return validOptions.isEmpty() ? super.getOptions(context, host) : validOptions; + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return StringArgument::new; + } + } + + private static class WordArgument extends StringArgument + { + private WordArgument() + { + super(); + suffix = "term"; + examples = StringArgumentType.StringType.SINGLE_WORD.getExamples(); + } + + @Override + public ArgumentType getArgumentType(CarpetScriptHost host) + { + return StringArgumentType.word(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return WordArgument::new; + } + } + + private static class GreedyStringArgument extends StringArgument + { + private GreedyStringArgument() + { + super(); + suffix = "text"; + examples = StringArgumentType.StringType.GREEDY_PHRASE.getExamples(); + } + + @Override + public ArgumentType getArgumentType(CarpetScriptHost host) + { + return StringArgumentType.greedyString(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return GreedyStringArgument::new; + } + } + + private static class BlockPosArgument extends CommandArgument + { + private boolean mustBeLoaded = false; + + private BlockPosArgument() + { + super("pos", net.minecraft.commands.arguments.coordinates.BlockPosArgument.blockPos().getExamples(), false); + } + + @Override + public ArgumentType getArgumentType(CarpetScriptHost host) + { + return net.minecraft.commands.arguments.coordinates.BlockPosArgument.blockPos(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + BlockPos pos = mustBeLoaded + ? net.minecraft.commands.arguments.coordinates.BlockPosArgument.getLoadedBlockPos(context, param) + : net.minecraft.commands.arguments.coordinates.BlockPosArgument.getSpawnablePos(context, param); + return ValueConversions.of(pos); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + mustBeLoaded = config.getOrDefault("loaded", Value.FALSE).getBoolean(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return BlockPosArgument::new; + } + } + + private static class LocationArgument extends CommandArgument + { + boolean blockCentered; + + private LocationArgument() + { + super("location", Vec3Argument.vec3().getExamples(), false); + blockCentered = true; + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + return Vec3Argument.vec3(blockCentered); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) + { + return ValueConversions.of(Vec3Argument.getVec3(context, param)); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + blockCentered = config.getOrDefault("block_centered", Value.TRUE).getBoolean(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return LocationArgument::new; + } + } + + private static class EntityArgument extends CommandArgument + { + boolean onlyFans; + boolean single; + + private EntityArgument() + { + super("entities", net.minecraft.commands.arguments.EntityArgument.entities().getExamples(), false); + onlyFans = false; + single = false; + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + if (onlyFans) + { + return single ? net.minecraft.commands.arguments.EntityArgument.player() : net.minecraft.commands.arguments.EntityArgument.players(); + } + return single ? net.minecraft.commands.arguments.EntityArgument.entity() : net.minecraft.commands.arguments.EntityArgument.entities(); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + Collection founds = net.minecraft.commands.arguments.EntityArgument.getOptionalEntities(context, param); + if (!single) + { + return ListValue.wrap(founds.stream().map(EntityValue::new)); + } + if (founds.isEmpty()) + { + return Value.NULL; + } + if (founds.size() == 1) + { + return new EntityValue(founds.iterator().next()); + } + throw new SimpleCommandExceptionType(Component.literal("Multiple entities returned while only one was requested" + " for custom type " + suffix)).create(); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + onlyFans = config.getOrDefault("players", Value.FALSE).getBoolean(); + single = config.getOrDefault("single", Value.FALSE).getBoolean(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return EntityArgument::new; + } + } + + private static class PlayerProfileArgument extends CommandArgument + { + boolean single; + + private PlayerProfileArgument() + { + super("players", GameProfileArgument.gameProfile().getExamples(), false); + single = false; + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + return GameProfileArgument.gameProfile(); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + Collection profiles = GameProfileArgument.getGameProfiles(context, param); + if (!single) + { + return ListValue.wrap(profiles.stream().map(p -> StringValue.of(p.getName()))); + } + int size = profiles.size(); + if (size == 0) + { + return Value.NULL; + } + if (size == 1) + { + return StringValue.of(profiles.iterator().next().getName()); + } + throw new SimpleCommandExceptionType(Component.literal("Multiple game profiles returned while only one was requested" + " for custom type " + suffix)).create(); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + single = config.getOrDefault("single", Value.FALSE).getBoolean(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return PlayerProfileArgument::new; + } + } + + private static class ScoreholderArgument extends CommandArgument + { + boolean single; + + private ScoreholderArgument() + { + super("scoreholder", ScoreHolderArgument.scoreHolder().getExamples(), false); + single = false; + suggestionProvider = param -> ScoreHolderArgument.SUGGEST_SCORE_HOLDERS; + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + return single ? ScoreHolderArgument.scoreHolder() : ScoreHolderArgument.scoreHolders(); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + Collection holders = ScoreHolderArgument.getNames(context, param); + if (!single) + { + return ListValue.wrap(holders.stream().map(ValueConversions::of)); + } + int size = holders.size(); + if (size == 0) + { + return Value.NULL; + } + if (size == 1) + { + return ValueConversions.of(holders.iterator().next()); + } + throw new SimpleCommandExceptionType(Component.literal("Multiple score holders returned while only one was requested" + " for custom type " + suffix)).create(); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + single = config.getOrDefault("single", Value.FALSE).getBoolean(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return PlayerProfileArgument::new; + } + } + + private static class TagArgument extends CommandArgument + { + boolean mapRequired; + + private TagArgument() + { + super("tag", CompoundTagArgument.compoundTag().getExamples(), false); + mapRequired = true; + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + return mapRequired ? CompoundTagArgument.compoundTag() : NbtTagArgument.nbtTag(); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) + { + return mapRequired + ? new NBTSerializableValue(CompoundTagArgument.getCompoundTag(context, param)) + : new NBTSerializableValue(NbtTagArgument.getNbtTag(context, param)); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + mapRequired = !config.getOrDefault("allow_element", Value.FALSE).getBoolean(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return TagArgument::new; + } + } + + private static class CustomIdentifierArgument extends CommandArgument + { + Set validOptions = Collections.emptySet(); + + protected CustomIdentifierArgument() + { + super("identifier", Collections.emptyList(), true); + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + return ResourceLocationArgument.id(); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + ResourceLocation choseValue = ResourceLocationArgument.getId(context, param); + if (!validOptions.isEmpty() && !validOptions.contains(choseValue)) + { + throw new SimpleCommandExceptionType(Component.literal("Incorrect value for " + param + ": " + choseValue + " for custom type " + suffix)).create(); + } + return ValueConversions.of(choseValue); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return CustomIdentifierArgument::new; + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + if (config.containsKey("options")) + { + Value optionsValue = config.get("options"); + if (!(optionsValue instanceof ListValue)) + { + throw error("Custom sting type requires options passed as a list" + " for custom type " + suffix); + } + validOptions = ((ListValue) optionsValue).getItems().stream().map(v -> ResourceLocation.parse(v.getString())).collect(Collectors.toSet()); + } + } + } + + private static class FloatArgument extends CommandArgument + { + private Double min = null; + private Double max = null; + + private FloatArgument() + { + super("float", DoubleArgumentType.doubleArg().getExamples(), true); + } + + @Override + public ArgumentType getArgumentType(CarpetScriptHost host) + { + if (min != null) + { + if (max != null) + { + return DoubleArgumentType.doubleArg(min, max); + } + return DoubleArgumentType.doubleArg(min); + } + return DoubleArgumentType.doubleArg(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) + { + return new NumericValue(DoubleArgumentType.getDouble(context, param)); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + if (config.containsKey("min")) + { + min = NumericValue.asNumber(config.get("min"), "min").getDouble(); + } + if (config.containsKey("max")) + { + max = NumericValue.asNumber(config.get("max"), "max").getDouble(); + } + if (max != null && min == null) + { + throw error("Double types cannot be only upper-bounded" + " for custom type " + suffix); + } + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return FloatArgument::new; + } + } + + private static class IntArgument extends CommandArgument + { + private Long min = null; + private Long max = null; + + private IntArgument() + { + super("int", LongArgumentType.longArg().getExamples(), true); + } + + @Override + public ArgumentType getArgumentType(CarpetScriptHost host) + { + if (min != null) + { + if (max != null) + { + return LongArgumentType.longArg(min, max); + } + return LongArgumentType.longArg(min); + } + return LongArgumentType.longArg(); + } + + @Override + public Value getValueFromContext(CommandContext context, String param) + { + return new NumericValue(LongArgumentType.getLong(context, param)); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + if (config.containsKey("min")) + { + min = NumericValue.asNumber(config.get("min"), "min").getLong(); + } + if (config.containsKey("max")) + { + max = NumericValue.asNumber(config.get("max"), "max").getLong(); + } + if (max != null && min == null) + { + throw error("Double types cannot be only upper-bounded" + " for custom type " + suffix); + } + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return IntArgument::new; + } + } + + private static class SlotArgument extends CommandArgument + { + private record ContainerIds(IntSet numericalIds, Set commandIds) + { + } + + private String restrict; + private static final Map RESTRICTED_CONTAINERS = new HashMap<>() + {{ + int i; + for (String source : Arrays.asList("player", "enderchest", "equipment", "armor", "weapon", "container", "villager", "horse")) + { + put(source, new ContainerIds(new IntOpenHashSet(), new HashSet<>())); + } + for (i = 0; i < 41; i++) + { + get("player").numericalIds().add(i); + } + for (i = 0; i < 41; i++) + { + get("player").commandIds().add("container." + i); + } + for (i = 0; i < 9; i++) + { + get("player").commandIds().add("hotbar." + i); + } + for (i = 0; i < 27; i++) + { + get("player").commandIds().add("inventory." + i); + } + for (String place : Arrays.asList("weapon", "weapon.mainhand", "weapon.offhand")) + { + get("player").commandIds().add(place); + get("equipment").commandIds().add(place); + get("weapon").commandIds().add(place); + } + for (String place : Arrays.asList("armor.feet", "armor.legs", "armor.chest", "armor.head")) + { + get("player").commandIds().add(place); + get("equipment").commandIds().add(place); + get("armor").commandIds().add(place); + } + + for (i = 0; i < 27; i++) + { + get("enderchest").numericalIds().add(200 + i); + } + for (i = 0; i < 27; i++) + { + get("enderchest").commandIds().add("enderchest." + i); + } + + for (i = 0; i < 6; i++) + { + get("equipment").numericalIds().add(98 + i); + } + + for (i = 0; i < 4; i++) + { + get("armor").numericalIds().add(100 + i); + } + + for (i = 0; i < 2; i++) + { + get("weapon").numericalIds().add(98 + i); + } + + for (i = 0; i < 54; i++) + { + get("container").numericalIds().add(i); + } + for (i = 0; i < 41; i++) + { + get("container").commandIds().add("container." + i); + } + + for (i = 0; i < 8; i++) + { + get("villager").numericalIds().add(i); + } + for (i = 0; i < 8; i++) + { + get("villager").commandIds().add("villager." + i); + } + + for (i = 0; i < 15; i++) + { + get("horse").numericalIds().add(500 + i); + } + for (i = 0; i < 15; i++) + { + get("horse").commandIds().add("horse." + i); + } + get("horse").numericalIds().add(400); + get("horse").commandIds().add("horse.saddle"); + get("horse").numericalIds().add(401); + get("horse").commandIds().add("horse.armor"); + }}; + + protected SlotArgument() + { + super("slot", net.minecraft.commands.arguments.SlotArgument.slot().getExamples(), false); + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) + { + return net.minecraft.commands.arguments.SlotArgument.slot(); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + int slot = net.minecraft.commands.arguments.SlotArgument.getSlot(context, param); + if (restrict != null && !RESTRICTED_CONTAINERS.get(restrict).numericalIds().contains(slot)) + { + throw new SimpleCommandExceptionType(Component.literal("Incorrect slot restricted to " + restrict + " for custom type " + suffix)).create(); + } + return ValueConversions.ofVanillaSlotResult(slot); + } + + @Override + protected void configure(Map config, CarpetScriptHost host) throws CommandSyntaxException + { + super.configure(config, host); + if (config.containsKey("restrict")) + { + restrict = config.get("restrict").getString().toLowerCase(Locale.ROOT); + needsMatching = true; + if (!RESTRICTED_CONTAINERS.containsKey(restrict)) + { + throw error("Incorrect slot restriction " + restrict + " for custom type " + suffix); + } + } + } + + @Override + protected Collection getOptions(CommandContext context, CarpetScriptHost host) throws CommandSyntaxException + { + return restrict == null ? super.getOptions(context, host) : RESTRICTED_CONTAINERS.get(restrict).commandIds(); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return SlotArgument::new; + } + } + + @FunctionalInterface + private interface ValueExtractor + { + Value apply(CommandContext ctx, String param) throws CommandSyntaxException; + } + + @FunctionalInterface + private interface ArgumentProvider + { + ArgumentType get() throws CommandSyntaxException; + } + + @FunctionalInterface + private interface ArgumentProviderEx + { + ArgumentType get(CommandBuildContext regAccess) throws CommandSyntaxException; + } + + public static class VanillaUnconfigurableArgument extends CommandArgument + { + private final ArgumentProvider argumentTypeSupplier; + private final ArgumentProviderEx argumentTypeSupplierEx; + private final ValueExtractor valueExtractor; + private final boolean providesExamples; + + public VanillaUnconfigurableArgument( + String suffix, + ArgumentProvider argumentTypeSupplier, + ValueExtractor valueExtractor, + boolean suggestFromExamples + ) + { + super(suffix, null, suggestFromExamples); + try + { + this.examples = argumentTypeSupplier.get().getExamples(); + } + catch (CommandSyntaxException e) + { + this.examples = Collections.emptyList(); + } + this.providesExamples = suggestFromExamples; + this.argumentTypeSupplier = argumentTypeSupplier; + this.valueExtractor = valueExtractor; + this.argumentTypeSupplierEx = null; + } + + public VanillaUnconfigurableArgument( + String suffix, + ArgumentProvider argumentTypeSupplier, + ValueExtractor valueExtractor, + SuggestionProvider suggester + ) + { + super(suffix, Collections.emptyList(), false); + this.suggestionProvider = param -> suggester; + this.providesExamples = false; + this.argumentTypeSupplier = argumentTypeSupplier; + this.valueExtractor = valueExtractor; + this.argumentTypeSupplierEx = null; + } + + public VanillaUnconfigurableArgument( + String suffix, + ArgumentProviderEx argumentTypeSupplier, + ValueExtractor valueExtractor, + boolean suggestFromExamples, + MinecraftServer server) + { + super(suffix, null, suggestFromExamples); + try + { + CommandBuildContext context = CommandBuildContext.simple(server.registryAccess(), server.getWorldData().enabledFeatures()); + this.examples = argumentTypeSupplier.get(context).getExamples(); + } + catch (CommandSyntaxException e) + { + this.examples = Collections.emptyList(); + } + this.providesExamples = suggestFromExamples; + this.argumentTypeSupplierEx = argumentTypeSupplier; + this.valueExtractor = valueExtractor; + this.argumentTypeSupplier = null; + } + + public VanillaUnconfigurableArgument( + String suffix, + ArgumentProviderEx argumentTypeSupplier, + ValueExtractor valueExtractor, + SuggestionProvider suggester + ) + { + super(suffix, Collections.emptyList(), false); + this.suggestionProvider = param -> suggester; + this.providesExamples = false; + this.argumentTypeSupplierEx = argumentTypeSupplier; + this.valueExtractor = valueExtractor; + this.argumentTypeSupplier = null; + } + + public VanillaUnconfigurableArgument( + String suffix, + ArgumentProviderEx argumentTypeSupplier, + ValueExtractor valueExtractor, + Function> suggesterGen + ) + { + super(suffix, Collections.emptyList(), false); + this.suggestionProvider = suggesterGen; + this.providesExamples = false; + this.argumentTypeSupplierEx = argumentTypeSupplier; + this.valueExtractor = valueExtractor; + this.argumentTypeSupplier = null; + } + + public VanillaUnconfigurableArgument( + String suffix, + ResourceKey> registry + ) + { + this( + suffix, + c -> ResourceArgument.resource(c, registry), + (c, p) -> ValueConversions.of(ResourceArgument.getResource(c, p, registry).key()), + (c, b) -> SharedSuggestionProvider.suggestResource(c.getSource().getServer().registryAccess().registryOrThrow(registry).keySet(), b) + ); + } + + @Override + protected ArgumentType getArgumentType(CarpetScriptHost host) throws CommandSyntaxException + { + return argumentTypeSupplier != null + ? argumentTypeSupplier.get() + : argumentTypeSupplierEx.get(CommandBuildContext.simple(host.scriptServer().server.registryAccess(), host.scriptServer().server.getWorldData().enabledFeatures())); + } + + @Override + protected Value getValueFromContext(CommandContext context, String param) throws CommandSyntaxException + { + return valueExtractor.apply(context, param); + } + + @Override + protected Supplier factory(MinecraftServer server) + { + return argumentTypeSupplier != null + ? (() -> new VanillaUnconfigurableArgument(getTypeSuffix(), argumentTypeSupplier, valueExtractor, providesExamples)) + : (() -> new VanillaUnconfigurableArgument(getTypeSuffix(), argumentTypeSupplierEx, valueExtractor, providesExamples, server)); + } + } +} diff --git a/src/main/java/carpet/script/command/CommandToken.java b/src/main/java/carpet/script/command/CommandToken.java new file mode 100644 index 0000000..22c06f4 --- /dev/null +++ b/src/main/java/carpet/script/command/CommandToken.java @@ -0,0 +1,126 @@ +package carpet.script.command; + +import carpet.script.CarpetScriptHost; +import carpet.script.value.FunctionValue; +import com.google.common.collect.Lists; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; + +import net.minecraft.commands.CommandSourceStack; + +import javax.annotation.Nullable; + +import static net.minecraft.commands.Commands.literal; + +public class CommandToken implements Comparable +{ + public final String surface; + public final boolean isArgument; + @Nullable + public final CommandArgument type; + + private CommandToken(String surface, @Nullable CommandArgument type) + { + this.surface = surface; + this.type = type; + isArgument = type != null; + } + + @Nullable + public static CommandToken getToken(String source, CarpetScriptHost host) + { + // todo add more type checking and return null + if (!source.startsWith("<")) + { + return source.matches("[_a-zA-Z]+") ? new CommandToken(source, null) : null; + } + source = source.substring(1, source.length() - 1); + return source.matches("[_a-zA-Z]+") ? new CommandToken(source, CommandArgument.getTypeForArgument(source, host)) : null; + } + + public static List parseSpec(String spec, CarpetScriptHost host) throws CommandSyntaxException + { + spec = spec.trim(); + if (spec.isEmpty()) + { + return Collections.emptyList(); + } + List elements = new ArrayList<>(); + HashSet seenArgs = new HashSet<>(); + for (String el : spec.split("\\s+")) + { + CommandToken tok = CommandToken.getToken(el, host); + if (tok == null) + { + throw CommandArgument.error("Unrecognized command token: " + el); + } + if (tok.isArgument) + { + if (seenArgs.contains(tok.surface)) + { + throw CommandArgument.error("Repeated command argument: " + tok.surface + ", for '" + spec + "'. Argument names have to be unique"); + } + seenArgs.add(tok.surface); + } + elements.add(tok); + } + return elements; + } + + public static String specFromSignature(FunctionValue function) + { + List tokens = Lists.newArrayList(function.getString()); + for (String arg : function.getArguments()) + { + tokens.add("<" + arg + ">"); + } + return String.join(" ", tokens); + } + + public ArgumentBuilder getCommandNode(CarpetScriptHost host) throws CommandSyntaxException + { + return isArgument ? CommandArgument.argumentNode(surface, host) : literal(surface); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + CommandToken that = (CommandToken) o; + return surface.equals(that.surface) && + Objects.equals(type, that.type); + } + + @Override + public int hashCode() + { + return Objects.hash(surface, type); + } + + @Override + public int compareTo(CommandToken o) + { + if (isArgument && !o.isArgument) + { + return 1; + } + if (!isArgument && o.isArgument) + { + return -1; + } + return surface.compareTo(o.surface); + } +} diff --git a/src/main/java/carpet/script/command/package-info.java b/src/main/java/carpet/script/command/package-info.java new file mode 100644 index 0000000..a49b08e --- /dev/null +++ b/src/main/java/carpet/script/command/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.command; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/exception/BreakStatement.java b/src/main/java/carpet/script/exception/BreakStatement.java new file mode 100644 index 0000000..40b76a1 --- /dev/null +++ b/src/main/java/carpet/script/exception/BreakStatement.java @@ -0,0 +1,13 @@ +package carpet.script.exception; + +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +public class BreakStatement extends ExitStatement +{ + public BreakStatement(@Nullable Value value) + { + super(value); + } +} diff --git a/src/main/java/carpet/script/exception/CarpetExpressionException.java b/src/main/java/carpet/script/exception/CarpetExpressionException.java new file mode 100644 index 0000000..a583c88 --- /dev/null +++ b/src/main/java/carpet/script/exception/CarpetExpressionException.java @@ -0,0 +1,30 @@ +package carpet.script.exception; + +import carpet.script.external.Carpet; +import carpet.script.value.FunctionValue; + +import java.util.List; + +import net.minecraft.commands.CommandSourceStack; + +public class CarpetExpressionException extends StacklessRuntimeException implements ResolvedException +{ + public final List stack; + + public CarpetExpressionException(String message, List stack) + { + super(message); + this.stack = stack; + } + + public void printStack(CommandSourceStack source) + { + if (stack != null && !stack.isEmpty()) + { + for (FunctionValue fun : stack) + { + Carpet.Messenger_message(source, "e ... in " + fun.fullName(), "e /" + (fun.getToken().lineno + 1) + ":" + (fun.getToken().linepos + 1)); + } + } + } +} diff --git a/src/main/java/carpet/script/exception/ContinueStatement.java b/src/main/java/carpet/script/exception/ContinueStatement.java new file mode 100644 index 0000000..449b15e --- /dev/null +++ b/src/main/java/carpet/script/exception/ContinueStatement.java @@ -0,0 +1,13 @@ +package carpet.script.exception; + +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +public class ContinueStatement extends ExitStatement +{ + public ContinueStatement(@Nullable Value value) + { + super(value); + } +} diff --git a/src/main/java/carpet/script/exception/ExitStatement.java b/src/main/java/carpet/script/exception/ExitStatement.java new file mode 100644 index 0000000..b198233 --- /dev/null +++ b/src/main/java/carpet/script/exception/ExitStatement.java @@ -0,0 +1,17 @@ +package carpet.script.exception; + +import carpet.script.value.Value; + +import javax.annotation.Nullable; + +/* Exception thrown to terminate execution mid expression (aka return statement) */ +public class ExitStatement extends StacklessRuntimeException +{ + @Nullable + public final Value retval; + + public ExitStatement(@Nullable Value value) + { + retval = value; + } +} diff --git a/src/main/java/carpet/script/exception/ExpressionException.java b/src/main/java/carpet/script/exception/ExpressionException.java new file mode 100644 index 0000000..d0ab165 --- /dev/null +++ b/src/main/java/carpet/script/exception/ExpressionException.java @@ -0,0 +1,101 @@ +package carpet.script.exception; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Tokenizer; +import carpet.script.external.Carpet; +import carpet.script.value.FunctionValue; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/* The expression evaluators exception class. */ +public class ExpressionException extends StacklessRuntimeException implements ResolvedException +{ + public final Context context; + public final Tokenizer.Token token; + public final List stack = new ArrayList<>(); + private final Supplier lazyMessage; + private String cachedMessage = null; + + public static void prepareForDoom() + { + Carpet.Messenger_compose("foo bar"); + } + + public ExpressionException(Context c, Expression e, String message) + { + this(c, e, Tokenizer.Token.NONE, message); + } + + public ExpressionException(Context c, Expression e, Tokenizer.Token t, String message) + { + this(c, e, t, message, Collections.emptyList()); + } + + public ExpressionException(Context c, Expression e, Tokenizer.Token t, String message, List stack) + { + super("Error"); + this.stack.addAll(stack); + lazyMessage = () -> makeMessage(c, e, t, message); + token = t; + context = c; + } + + public ExpressionException(Context c, Expression e, Tokenizer.Token t, Supplier messageSupplier, List stack) + { + super("Error"); + this.stack.addAll(stack); + lazyMessage = () -> makeMessage(c, e, t, messageSupplier.get()); + token = t; + context = c; + } + + private static List makeError(Expression expr, @Nullable Tokenizer.Token token, String errmessage) + { + List errMsg = new ArrayList<>(); + errmessage += expr.getModuleName() == null ? "" : (" in " + expr.getModuleName()); + if (token != null) + { + List snippet = expr.getExpressionSnippet(token); + errMsg.addAll(snippet); + + if (snippet.size() != 1) + { + errmessage += " at line " + (token.lineno + 1) + ", pos " + (token.linepos + 1); + } + else + { + errmessage += " at pos " + (token.pos + 1); + } + } + errMsg.add(errmessage); + return errMsg; + } + + static synchronized String makeMessage(Context c, Expression e, Tokenizer.Token t, String message) throws ExpressionException + { + if (c.getErrorSnooper() != null) + { + List alternative = c.getErrorSnooper().apply(e, t, c, message); + if (alternative != null) + { + return String.join("\n", alternative); + } + } + return String.join("\n", makeError(e, t, message)); + } + + @Override + public String getMessage() + { + if (cachedMessage == null) + { + cachedMessage = lazyMessage.get(); + } + return cachedMessage; + } +} diff --git a/src/main/java/carpet/script/exception/IntegrityException.java b/src/main/java/carpet/script/exception/IntegrityException.java new file mode 100644 index 0000000..262c57f --- /dev/null +++ b/src/main/java/carpet/script/exception/IntegrityException.java @@ -0,0 +1,9 @@ +package carpet.script.exception; + +public class IntegrityException extends RuntimeException +{ + public IntegrityException(String message) + { + super(message); + } +} diff --git a/src/main/java/carpet/script/exception/InternalExpressionException.java b/src/main/java/carpet/script/exception/InternalExpressionException.java new file mode 100644 index 0000000..ef850ed --- /dev/null +++ b/src/main/java/carpet/script/exception/InternalExpressionException.java @@ -0,0 +1,37 @@ +package carpet.script.exception; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Tokenizer; +import carpet.script.value.FunctionValue; + +import java.util.ArrayList; +import java.util.List; + +/* The internal expression evaluators exception class. */ +public class InternalExpressionException extends StacklessRuntimeException +{ + public List stack = new ArrayList<>(); + + public InternalExpressionException(String message) + { + super(message); + } + + /** + *

Promotes this simple exception into one with context and extra information. + * + *

Provides a cleaner way of handling similar exceptions, in this case + * {@link InternalExpressionException} and {@link ThrowStatement} + * + * @param c Context + * @param e Expression + * @param token Token + * @return The new {@link ExpressionException} (or {@link ProcessedThrowStatement}), + * depending on the implementation. + */ + public ExpressionException promote(Context c, Expression e, Tokenizer.Token token) + { + return new ExpressionException(c, e, token, getMessage(), stack); + } +} diff --git a/src/main/java/carpet/script/exception/InvalidCallbackException.java b/src/main/java/carpet/script/exception/InvalidCallbackException.java new file mode 100644 index 0000000..3a8ca68 --- /dev/null +++ b/src/main/java/carpet/script/exception/InvalidCallbackException.java @@ -0,0 +1,5 @@ +package carpet.script.exception; + +public class InvalidCallbackException extends Exception +{ +} diff --git a/src/main/java/carpet/script/exception/LoadException.java b/src/main/java/carpet/script/exception/LoadException.java new file mode 100644 index 0000000..54245a8 --- /dev/null +++ b/src/main/java/carpet/script/exception/LoadException.java @@ -0,0 +1,19 @@ +package carpet.script.exception; + +/** + * A Scarpet exception that indicates that load of the app has failed. + *

+ * Goes up the stack to the point of app load and gets caught there, preventing the app from loading with + * the given message. + */ +public class LoadException extends RuntimeException implements ResolvedException +{ + public LoadException() + { + super(); + } + public LoadException(String message) + { + super(message); + } +} diff --git a/src/main/java/carpet/script/exception/ProcessedThrowStatement.java b/src/main/java/carpet/script/exception/ProcessedThrowStatement.java new file mode 100644 index 0000000..3768550 --- /dev/null +++ b/src/main/java/carpet/script/exception/ProcessedThrowStatement.java @@ -0,0 +1,22 @@ +package carpet.script.exception; + +import java.util.List; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Tokenizer.Token; +import carpet.script.value.FunctionValue; +import carpet.script.value.Value; + +public class ProcessedThrowStatement extends ExpressionException +{ + public final Throwables thrownExceptionType; + public final Value data; + + public ProcessedThrowStatement(Context c, Expression e, Token token, List stack, Throwables thrownExceptionType, Value data) + { + super(c, e, token, () -> "Unhandled " + thrownExceptionType.getId() + " exception: " + data.getString(), stack); + this.thrownExceptionType = thrownExceptionType; + this.data = data; + } +} diff --git a/src/main/java/carpet/script/exception/ResolvedException.java b/src/main/java/carpet/script/exception/ResolvedException.java new file mode 100644 index 0000000..dc75c31 --- /dev/null +++ b/src/main/java/carpet/script/exception/ResolvedException.java @@ -0,0 +1,5 @@ +package carpet.script.exception; + +public interface ResolvedException +{ +} diff --git a/src/main/java/carpet/script/exception/ReturnStatement.java b/src/main/java/carpet/script/exception/ReturnStatement.java new file mode 100644 index 0000000..a1ed7a6 --- /dev/null +++ b/src/main/java/carpet/script/exception/ReturnStatement.java @@ -0,0 +1,11 @@ +package carpet.script.exception; + +import carpet.script.value.Value; + +public class ReturnStatement extends ExitStatement +{ + public ReturnStatement(Value value) + { + super(value); + } +} diff --git a/src/main/java/carpet/script/exception/StacklessRuntimeException.java b/src/main/java/carpet/script/exception/StacklessRuntimeException.java new file mode 100644 index 0000000..476e6ae --- /dev/null +++ b/src/main/java/carpet/script/exception/StacklessRuntimeException.java @@ -0,0 +1,23 @@ +package carpet.script.exception; + +/** + * A type of {@link RuntimeException} that doesn't spend time producing and filling a stacktrace + */ +public abstract class StacklessRuntimeException extends RuntimeException +{ + public StacklessRuntimeException() + { + super(); + } + + public StacklessRuntimeException(String message) + { + super(message); + } + + @Override + public Throwable fillInStackTrace() + { + return this; + } +} diff --git a/src/main/java/carpet/script/exception/ThrowStatement.java b/src/main/java/carpet/script/exception/ThrowStatement.java new file mode 100644 index 0000000..40cf10b --- /dev/null +++ b/src/main/java/carpet/script/exception/ThrowStatement.java @@ -0,0 +1,57 @@ +package carpet.script.exception; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Tokenizer.Token; +import carpet.script.value.StringValue; +import carpet.script.value.Value; + +public class ThrowStatement extends InternalExpressionException +{ + private final Throwables type; + private final Value data; + + /** + * Creates a throw exception from a value, and assigns it a specified message. + *

To be used when throwing from Scarpet's {@code throw} function + * + * @param data The value to pass + * @param type Exception type + */ + public ThrowStatement(Value data, Throwables type) + { + super(type.getId()); + this.data = data; + this.type = type; + } + + public ThrowStatement(Value data, Throwables parent, String subtype) + { + super(subtype); + this.data = data; + this.type = new Throwables(subtype, parent); + } + + /** + * Creates a throw exception.
+ * Conveniently creates a value from the {@code value} String + * to be used easily in Java code + * + * @param message The message to display when not handled + * @param type An {@link Throwables} containing the inheritance data + * for this exception. When throwing from Java, + * those exceptions should be pre-registered. + */ + public ThrowStatement(String message, Throwables type) + { + super(type.getId()); + this.data = StringValue.of(message); + this.type = type; + } + + @Override + public ExpressionException promote(Context c, Expression e, Token token) + { + return new ProcessedThrowStatement(c, e, token, stack, type, data); + } +} diff --git a/src/main/java/carpet/script/exception/Throwables.java b/src/main/java/carpet/script/exception/Throwables.java new file mode 100644 index 0000000..ab34594 --- /dev/null +++ b/src/main/java/carpet/script/exception/Throwables.java @@ -0,0 +1,101 @@ +package carpet.script.exception; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +/** + * This class contains default Scarpet catchable types, as well as their inheritance and + * methods to check whether filters are compatible with those. + */ +public class Throwables +{ + private final String id; + @Nullable + private final Throwables parent; + + private static final Map byId = new HashMap<>(); + + public static final Throwables THROWN_EXCEPTION_TYPE = register("exception", null); + public static final Throwables VALUE_EXCEPTION = register("value_exception", THROWN_EXCEPTION_TYPE); + public static final Throwables UNKNOWN_ITEM = register("unknown_item", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_BLOCK = register("unknown_block", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_BIOME = register("unknown_biome", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_PARTICLE = register("unknown_particle", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_POI = register("unknown_poi", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_DIMENSION = register("unknown_dimension", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_STRUCTURE = register("unknown_structure", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_CRITERION = register("unknown_criterion", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_SCREEN = register("unknown_screen", VALUE_EXCEPTION); + public static final Throwables IO_EXCEPTION = register("io_exception", THROWN_EXCEPTION_TYPE); + public static final Throwables NBT_ERROR = register("nbt_error", IO_EXCEPTION); + public static final Throwables JSON_ERROR = register("json_error", IO_EXCEPTION); + public static final Throwables B64_ERROR = register("b64_error", IO_EXCEPTION); + public static final Throwables USER_DEFINED = register("user_exception", THROWN_EXCEPTION_TYPE); + + /** + * Creates an exception and registers it to be used as parent for + * user defined exceptions in Scarpet's throw function. + *

Scarpet exceptions should have a top-level parent being {@link Throwables#THROWN_EXCEPTION_TYPE} + * + * @param id The value for the exception as a {@link String}. + * @param parent The parent of the exception being created, or null if top-level + * @return The created exception + */ + public static Throwables register(String id, @Nullable Throwables parent) + { + Throwables exc = new Throwables(id, parent); + byId.put(id, exc); + return exc; + } + + /** + * Creates a new exception. + *

Not suitable for creating exceptions that can't be caught. + * Use an {@link InternalExpressionException} for that + * + * @param id The exception's value as a {@link String} + */ + public Throwables(String id, @Nullable Throwables parent) + { + this.id = id; + this.parent = parent; + } + + public static Throwables getTypeForException(String type) + { + Throwables properType = byId.get(type); + if (properType == null) + { + throw new InternalExpressionException("Unknown exception type: " + type); + } + return properType; + } + + /** + * Checks whether the given filter matches an instance of this exception, by checking equality + * with itself and possible parents. + * + * @param filter The type to check against + * @return Whether or not the given value matches this exception's hierarchy + */ + public boolean isRelevantFor(String filter) + { + return (id.equals(filter) || (parent != null && parent.isRelevantFor(filter))); + } + + public boolean isUserException() + { + return this == USER_DEFINED || parent == USER_DEFINED; + } + + /** + * Returns the throwable type + * + * @return The type of this exception + */ + public String getId() + { + return id; + } +} diff --git a/src/main/java/carpet/script/exception/package-info.java b/src/main/java/carpet/script/exception/package-info.java new file mode 100644 index 0000000..cb4c4d3 --- /dev/null +++ b/src/main/java/carpet/script/exception/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.exception; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/external/Carpet.java b/src/main/java/carpet/script/external/Carpet.java new file mode 100644 index 0000000..a6fdc1c --- /dev/null +++ b/src/main/java/carpet/script/external/Carpet.java @@ -0,0 +1,237 @@ +package carpet.script.external; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.api.settings.CarpetRule; +import carpet.api.settings.RuleHelper; +import carpet.api.settings.SettingsManager; +import carpet.fakes.MinecraftServerInterface; +import carpet.logging.HUDController; +import carpet.network.ServerNetworkHandler; +import carpet.patches.EntityPlayerMPFake; +import carpet.script.CarpetEventServer; +import carpet.script.CarpetExpression; +import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; +import carpet.script.Module; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.LoadException; +import carpet.script.value.MapValue; +import carpet.script.value.StringValue; +import carpet.utils.CarpetProfiler; +import carpet.utils.Messenger; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.fabricmc.loader.api.SemanticVersion; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +public class Carpet +{ + public static Map getScarpetHeaders() + { + return HUDController.scarpet_headers; + } + + public static Map getScarpetFooters() + { + return HUDController.scarpet_footers; + } + + public static void updateScarpetHUDs(MinecraftServer server, List players) + { + HUDController.update_hud(server, players); + } + + public static Component Messenger_compose(Object... messages) + { + return Messenger.c(messages); + } + + public static void Messenger_message(CommandSourceStack source, Object... messages) + { + Messenger.m(source, messages); + } + + public static ThreadLocal getImpendingFillSkipUpdates() + { + return CarpetSettings.impendingFillSkipUpdates; + } + + public static Runnable startProfilerSection(String name) + { + CarpetProfiler.ProfilerToken token = CarpetProfiler.start_section(null, name, CarpetProfiler.TYPE.GENERAL); + return () -> CarpetProfiler.end_current_section(token); + } + + public static void MinecraftServer_addScriptServer(MinecraftServer server, CarpetScriptServer scriptServer) + { + ((MinecraftServerInterface) server).addScriptServer(scriptServer); + } + + public static boolean isValidCarpetPlayer(ServerPlayer player) + { + return ServerNetworkHandler.isValidCarpetPlayer(player); + } + + public static String getPlayerStatus(ServerPlayer player) + { + return ServerNetworkHandler.getPlayerStatus(player); + } + + public static MapValue getAllCarpetRules() + { + Collection> rules = CarpetServer.settingsManager.getCarpetRules(); + MapValue carpetRules = new MapValue(Collections.emptyList()); + rules.forEach(rule -> carpetRules.put(new StringValue(rule.name()), new StringValue(RuleHelper.toRuleString(rule.value())))); + CarpetServer.extensions.forEach(e -> { + SettingsManager manager = e.extensionSettingsManager(); + if (manager == null) + { + return; + } + manager.getCarpetRules().forEach(rule -> carpetRules.put(new StringValue(manager.identifier() + ":" + rule.name()), new StringValue(RuleHelper.toRuleString(rule.value())))); + }); + return carpetRules; + } + + public static String getCarpetVersion() + { + return CarpetSettings.carpetVersion; + } + + @Nullable + public static String isModdedPlayer(Player p) + { + if (p instanceof final EntityPlayerMPFake fake) + { + return fake.isAShadow ? "shadow" : "fake"; + } + return null; + } + + public static void handleExtensionsAPI(CarpetExpression expression) + { + CarpetServer.extensions.forEach(e -> e.scarpetApi(expression)); + } + + public static boolean getFillUpdates() + { + return CarpetSettings.fillUpdates; + } + + @Nullable + public static Module fetchGlobalModule(String name, boolean allowLibraries) throws IOException + { + if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) + { + Path globalFolder = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts"); + if (!Files.exists(globalFolder)) + { + Files.createDirectories(globalFolder); + } + try (Stream folderWalker = Files.walk(globalFolder)) + { + Optional scriptPath = folderWalker + .filter(script -> script.getFileName().toString().equalsIgnoreCase(name + ".sc") || + (allowLibraries && script.getFileName().toString().equalsIgnoreCase(name + ".scl"))) + .findFirst(); + if (scriptPath.isPresent()) + { + return Module.fromPath(scriptPath.get()); + } + } + } + return null; + } + + public static void addGlobalModules(final List moduleNames, boolean includeBuiltIns) throws IOException + { + if (includeBuiltIns && (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)) + { + Path globalScripts = FabricLoader.getInstance().getConfigDir().resolve("carpet/scripts"); + if (!Files.exists(globalScripts)) + { + Files.createDirectories(globalScripts); + } + try (Stream folderWalker = Files.walk(globalScripts, FileVisitOption.FOLLOW_LINKS)) + { + folderWalker + .filter(f -> f.toString().endsWith(".sc")) + .forEach(f -> moduleNames.add(f.getFileName().toString().replaceFirst("\\.sc$", "").toLowerCase(Locale.ROOT))); + } + } + } + + public static void assertRequirementMet(CarpetScriptHost host, String requiredModId, String stringPredicate) + { + VersionPredicate predicate; + try + { + predicate = VersionPredicate.parse(stringPredicate); + } + catch (VersionParsingException e) + { + throw new InternalExpressionException("Failed to parse version conditions for '" + requiredModId + "' in 'requires': " + e.getMessage()); + } + + ModContainer mod = FabricLoader.getInstance().getModContainer(requiredModId).orElse(null); + if (mod != null) + { + Version presentVersion = mod.getMetadata().getVersion(); + if (predicate.test(presentVersion) || (FabricLoader.getInstance().isDevelopmentEnvironment() && !(presentVersion instanceof SemanticVersion))) + { // in a dev env, mod version is usually replaced with ${version}, and that isn't semantic + return; + } + } + throw new LoadException(String.format("%s requires a version of mod '%s' matching '%s', which is missing!", host.getVisualName(), requiredModId, stringPredicate)); + } + + // to be ran once during CarpetEventServer.Event static init + public static void initCarpetEvents() { + CarpetEventServer.Event carpetRuleChanges = new CarpetEventServer.Event("carpet_rule_changes", 2, true) + { + @Override + public void handleAny(final Object... args) + { + CarpetRule rule = (CarpetRule) args[0]; + CommandSourceStack source = (CommandSourceStack) args[1]; + String id = rule.settingsManager().identifier(); + String namespace; + if (!id.equals("carpet")) + { + namespace = id + ":"; + } else + { + namespace = ""; + } + handler.call(() -> Arrays.asList( + new StringValue(namespace + rule.name()), + new StringValue(RuleHelper.toRuleString(rule.value())) + ), () -> source); + } + }; + SettingsManager.registerGlobalRuleObserver((source, changedRule, userInput) -> carpetRuleChanges.handleAny(changedRule, source)); + } +} diff --git a/src/main/java/carpet/script/external/Vanilla.java b/src/main/java/carpet/script/external/Vanilla.java new file mode 100644 index 0000000..f046ae6 --- /dev/null +++ b/src/main/java/carpet/script/external/Vanilla.java @@ -0,0 +1,368 @@ +package carpet.script.external; + +import carpet.CarpetSettings; +import carpet.fakes.BiomeInterface; +import carpet.fakes.BlockPredicateInterface; +import carpet.fakes.BlockStateArgumentInterface; +import carpet.fakes.ChunkTicketManagerInterface; +import carpet.fakes.CommandDispatcherInterface; +import carpet.fakes.EntityInterface; +import carpet.fakes.IngredientInterface; +import carpet.fakes.InventoryBearerInterface; +import carpet.fakes.ItemEntityInterface; +import carpet.fakes.LivingEntityInterface; +import carpet.fakes.MinecraftServerInterface; +import carpet.fakes.MobEntityInterface; +import carpet.fakes.RandomStateVisitorAccessor; +import carpet.fakes.RecipeManagerInterface; +import carpet.fakes.AbstractContainerMenuInterface; +import carpet.fakes.ServerPlayerInterface; +import carpet.fakes.ServerPlayerInteractionManagerInterface; +import carpet.fakes.ServerWorldInterface; +import carpet.fakes.SpawnHelperInnerInterface; +import carpet.fakes.ThreadedAnvilChunkStorageInterface; +import carpet.mixins.Objective_scarpetMixin; +import carpet.mixins.PoiRecord_scarpetMixin; +import carpet.mixins.Scoreboard_scarpetMixin; +import carpet.network.ServerNetworkHandler; +import carpet.script.CarpetScriptServer; +import carpet.script.EntityEventsGroup; +import carpet.script.value.MapValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.utils.CommandHelper; +import carpet.utils.SpawnReporter; +import com.mojang.brigadier.CommandDispatcher; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.blocks.BlockInput; +import net.minecraft.core.BlockPos; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.server.level.Ticket; +import net.minecraft.tags.TagKey; +import net.minecraft.util.SortedArraySet; +import net.minecraft.world.Container; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.goal.GoalSelector; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.PotentialCalculator; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.level.levelgen.DensityFunction; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.Predicate; + +public class Vanilla +{ + public static void MinecraftServer_forceTick(MinecraftServer server, BooleanSupplier sup) + { + ((MinecraftServerInterface) server).forceTick(sup); + } + + public static void ChunkMap_relightChunk(ChunkMap chunkMap, ChunkPos pos) + { + //((ThreadedAnvilChunkStorageInterface) chunkMap).relightChunk(pos); + } + + public static Map ChunkMap_regenerateChunkRegion(ChunkMap chunkMap, List requestedChunks) + { + return Map.of(); //return ((ThreadedAnvilChunkStorageInterface) chunkMap).regenerateChunkRegion(requestedChunks); + } + + public static List> Ingredient_getRecipeStacks(Ingredient ingredient) + { + return ((IngredientInterface) (Object) ingredient).getRecipeStacks(); + } + + public static List> RecipeManager_getAllMatching(RecipeManager recipeManager, RecipeType type, ResourceLocation output, RegistryAccess registryAccess) + { + return ((RecipeManagerInterface) recipeManager).getAllMatching(type, output, registryAccess); + } + + public static int NaturalSpawner_MAGIC_NUMBER() + { + return SpawnReporter.MAGIC_NUMBER; + } + + public static PotentialCalculator SpawnState_getPotentialCalculator(NaturalSpawner.SpawnState spawnState) + { + return ((SpawnHelperInnerInterface) spawnState).getPotentialCalculator(); + } + + public static void Objective_setCriterion(Objective objective, ObjectiveCriteria criterion) + { + ((Objective_scarpetMixin) objective).setCriterion(criterion); + } + + public static Map> Scoreboard_getObjectivesByCriterion(Scoreboard scoreboard) + { + return ((Scoreboard_scarpetMixin) scoreboard).getObjectivesByCriterion(); + } + + public static ServerLevelData ServerLevel_getWorldProperties(ServerLevel world) + { + return ((ServerWorldInterface) world).getWorldPropertiesCM(); + } + + public static Long2ObjectOpenHashMap>> ChunkTicketManager_getTicketsByPosition(DistanceManager ticketManager) + { + return ((ChunkTicketManagerInterface) ticketManager).getTicketsByPosition(); + } + + public static DensityFunction.Visitor RandomState_getVisitor(RandomState randomState) + { + return ((RandomStateVisitorAccessor) (Object) randomState).getVisitor(); + } + + public static CompoundTag BlockInput_getTag(BlockInput blockInput) + { + return ((BlockStateArgumentInterface) blockInput).getCMTag(); + } + + public static CarpetScriptServer MinecraftServer_getScriptServer(MinecraftServer server) + { + return ((MinecraftServerInterface) server).getScriptServer(); + } + + public static Biome.ClimateSettings Biome_getClimateSettings(Biome biome) + { + return ((BiomeInterface) (Object) biome).getClimateSettings(); + } + + public static ThreadLocal skipGenerationChecks(ServerLevel level) + { // not sure does vanilla care at all - needs checking + return CarpetSettings.skipGenerationChecks; + } + + public static void sendScarpetShapesDataToPlayer(ServerPlayer player, Tag data) + { // dont forget to add the packet to vanilla packed handler and call ShapesRenderer.addShape to handle on client + ServerNetworkHandler.sendCustomCommand(player, "scShapes", data); + } + + public static int MinecraftServer_getRunPermissionLevel(MinecraftServer server) + { + return CarpetSettings.runPermissionLevel; + } + + public static int [] MinecraftServer_getReleaseTarget(MinecraftServer server) + { + return CarpetSettings.releaseTarget; + } + + public static boolean isDevelopmentEnvironment() + { + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } + + public static MapValue getServerMods(MinecraftServer server) + { + Map ret = new HashMap<>(); + for (ModContainer mod : FabricLoader.getInstance().getAllMods()) + { + ret.put(new StringValue(mod.getMetadata().getId()), new StringValue(mod.getMetadata().getVersion().getFriendlyString())); + } + return MapValue.wrap(ret); + } + + public static LevelStorageSource.LevelStorageAccess MinecraftServer_storageSource(MinecraftServer server) + { + return ((MinecraftServerInterface) server).getCMSession(); + } + + public static BlockPos ServerPlayerGameMode_getCurrentBlockPosition(ServerPlayerGameMode gameMode) + { + return ((ServerPlayerInteractionManagerInterface) gameMode).getCurrentBreakingBlock(); + } + + public static int ServerPlayerGameMode_getCurrentBlockBreakingProgress(ServerPlayerGameMode gameMode) + { + return ((ServerPlayerInteractionManagerInterface) gameMode).getCurrentBlockBreakingProgress(); + } + + public static void ServerPlayerGameMode_setBlockBreakingProgress(ServerPlayerGameMode gameMode, int progress) + { + ((ServerPlayerInteractionManagerInterface) gameMode).setBlockBreakingProgress(progress); + } + + public static boolean ServerPlayer_isInvalidEntityObject(ServerPlayer player) + { + return ((ServerPlayerInterface) player).isInvalidEntityObject(); + } + + public static String ServerPlayer_getLanguage(ServerPlayer player) + { + return player.clientInformation().language(); + } + + public static GoalSelector Mob_getAI(Mob mob, boolean target) + { + return ((MobEntityInterface) mob).getAI(target); + } + + public static Map Mob_getTemporaryTasks(Mob mob) + { + return ((MobEntityInterface) mob).getTemporaryTasks(); + } + + public static void Mob_setPersistence(Mob mob, boolean what) + { + ((MobEntityInterface) mob).setPersistence(what); + } + + public static EntityEventsGroup Entity_getEventContainer(Entity entity) + { + return ((EntityInterface) entity).getEventContainer(); + } + + public static boolean Entity_isPermanentVehicle(Entity entity) + { + return ((EntityInterface) entity).isPermanentVehicle(); + } + + public static void Entity_setPermanentVehicle(Entity entity, boolean permanent) + { + ((EntityInterface) entity).setPermanentVehicle(permanent); + } + + public static int Entity_getPortalTimer(Entity entity) + { + return ((EntityInterface) entity).getPortalTimer(); + } + + public static void Entity_setPortalTimer(Entity entity, int amount) + { + ((EntityInterface) entity).setPortalTimer(amount); + } + + public static int Entity_getPublicNetherPortalCooldown(Entity entity) + { + return ((EntityInterface) entity).getPublicNetherPortalCooldown(); + } + + public static void Entity_setPublicNetherPortalCooldown(Entity entity, int what) + { + ((EntityInterface) entity).setPublicNetherPortalCooldown(what); + } + + public static int ItemEntity_getPickupDelay(ItemEntity entity) + { + return ((ItemEntityInterface) entity).getPickupDelayCM(); + } + + public static boolean LivingEntity_isJumping(LivingEntity entity) + { + return ((LivingEntityInterface) entity).isJumpingCM(); + } + + public static void LivingEntity_setJumping(LivingEntity entity) + { + ((LivingEntityInterface) entity).doJumpCM(); + } + + public static Container AbstractHorse_getInventory(AbstractHorse horse) + { + return ((InventoryBearerInterface) horse).getCMInventory(); + } + + public static DataSlot AbstractContainerMenu_getDataSlot(AbstractContainerMenu handler, int index) + { + return ((AbstractContainerMenuInterface) handler).getDataSlot(index); + } + + public static void CommandDispatcher_unregisterCommand(CommandDispatcher dispatcher, String name) + { + ((CommandDispatcherInterface) dispatcher).carpet$unregister(name); + } + + public static boolean MinecraftServer_doScriptsAutoload(MinecraftServer server) + { + return CarpetSettings.scriptsAutoload; + } + + public static void MinecraftServer_notifyPlayersCommandsChanged(MinecraftServer server) + { + CommandHelper.notifyPlayersCommandsChanged(server); + } + + public static boolean ScriptServer_scriptOptimizations(MinecraftServer scriptServer) + { + return CarpetSettings.scriptsOptimization; + } + + public static boolean ScriptServer_scriptDebugging(MinecraftServer server) + { + return CarpetSettings.scriptsDebugging; + } + + public static boolean ServerPlayer_canScriptACE(CommandSourceStack player) + { + return CommandHelper.canUseCommand(player, CarpetSettings.commandScriptACE); + } + + public static boolean ServerPlayer_canScriptGeneral(CommandSourceStack player) + { + return CommandHelper.canUseCommand(player, CarpetSettings.commandScript); + } + + public static int MinecraftServer_getFillLimit(MinecraftServer server) + { + return server.getGameRules().getInt(GameRules.RULE_COMMAND_MODIFICATION_BLOCK_LIMIT); + } + + public static int PoiRecord_getFreeTickets(PoiRecord record) + { + return ((PoiRecord_scarpetMixin) record).getFreeTickets(); + } + + public static void PoiRecord_callAcquireTicket(PoiRecord record) + { + ((PoiRecord_scarpetMixin) record).callAcquireTicket(); + } + + public record BlockPredicatePayload(BlockState state, TagKey tagKey, Map properties, CompoundTag tag) { + public static BlockPredicatePayload of(Predicate blockPredicate) + { + BlockPredicateInterface predicateData = (BlockPredicateInterface) blockPredicate; + return new BlockPredicatePayload(predicateData.getCMBlockState(), predicateData.getCMBlockTagKey(), predicateData.getCMProperties(), predicateData.getCMDataTag()); + } + } +} diff --git a/src/main/java/carpet/script/external/VanillaClient.java b/src/main/java/carpet/script/external/VanillaClient.java new file mode 100644 index 0000000..874d15f --- /dev/null +++ b/src/main/java/carpet/script/external/VanillaClient.java @@ -0,0 +1,13 @@ +package carpet.script.external; + +import carpet.mixins.ShulkerBoxAccessMixin; +import net.minecraft.client.model.ShulkerModel; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity; + +public class VanillaClient +{ + public static ShulkerModel ShulkerBoxRenderer_model(BlockEntityRenderer shulkerBoxRenderer) { + return ((ShulkerBoxAccessMixin)shulkerBoxRenderer).getModel(); + } +} diff --git a/src/main/java/carpet/script/external/package-info.java b/src/main/java/carpet/script/external/package-info.java new file mode 100644 index 0000000..2e80a81 --- /dev/null +++ b/src/main/java/carpet/script/external/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.external; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/carpet/script/language/Arithmetic.java b/src/main/java/carpet/script/language/Arithmetic.java new file mode 100644 index 0000000..ca649ab --- /dev/null +++ b/src/main/java/carpet/script/language/Arithmetic.java @@ -0,0 +1,146 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.BooleanValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; + +public class Arithmetic +{ + public static final Value PI = new NumericValue(Math.PI); + public static final Value euler = new NumericValue(Math.E); + + public static void apply(Expression expression) + { + expression.addTypedContextFunction("not", 1, Context.Type.BOOLEAN, (c, t, lv) -> BooleanValue.of(lv.get(0).getBoolean())); + expression.addUnaryFunction("fact", v -> + { + long number = NumericValue.asNumber(v).getLong(); + if (number < 21) + { + long factorial = 1; + for (int i = 1; i <= number; i++) + { + factorial = factorial * i; + } + return new NumericValue(factorial); + } + else if (number > 170) + { + return NumericValue.of(Double.MAX_VALUE); + } + // values over 21 will exceed long limits + double factorial = 1.0; + for (int i = 1; i <= number; i++) + { + factorial = factorial * i; + } + return new NumericValue(factorial); + + }); + expression.addMathematicalUnaryFunction("sin", d -> Math.sin(Math.toRadians(d))); + expression.addMathematicalUnaryFunction("cos", d -> Math.cos(Math.toRadians(d))); + expression.addMathematicalUnaryFunction("tan", d -> Math.tan(Math.toRadians(d))); + expression.addMathematicalUnaryFunction("asin", d -> Math.toDegrees(Math.asin(d))); + expression.addMathematicalUnaryFunction("acos", d -> Math.toDegrees(Math.acos(d))); + expression.addMathematicalUnaryFunction("atan", d -> Math.toDegrees(Math.atan(d))); + expression.addMathematicalBinaryFunction("atan2", (d, d2) -> Math.toDegrees(Math.atan2(d, d2))); + expression.addMathematicalUnaryFunction("sinh", Math::sinh); + expression.addMathematicalUnaryFunction("cosh", Math::cosh); + expression.addMathematicalUnaryFunction("tanh", Math::tanh); + expression.addMathematicalUnaryFunction("sec", d -> 1.0 / Math.cos(Math.toRadians(d))); // Formula: sec(x) = 1 / cos(x) + expression.addMathematicalUnaryFunction("csc", d -> 1.0 / Math.sin(Math.toRadians(d))); // Formula: csc(x) = 1 / sin(x) + expression.addMathematicalUnaryFunction("sech", d -> 1.0 / Math.cosh(d)); // Formula: sech(x) = 1 / cosh(x) + expression.addMathematicalUnaryFunction("csch", d -> 1.0 / Math.sinh(d)); // Formula: csch(x) = 1 / sinh(x) + expression.addMathematicalUnaryFunction("cot", d -> 1.0 / Math.tan(Math.toRadians(d))); // Formula: cot(x) = cos(x) / sin(x) = 1 / tan(x) + expression.addMathematicalUnaryFunction("acot", d -> Math.toDegrees(Math.atan(1.0 / d)));// Formula: acot(x) = atan(1/x) + expression.addMathematicalUnaryFunction("coth", d -> 1.0 / Math.tanh(d)); // Formula: coth(x) = 1 / tanh(x) + expression.addMathematicalUnaryFunction("asinh", d -> Math.log(d + (Math.sqrt(Math.pow(d, 2) + 1)))); // Formula: asinh(x) = ln(x + sqrt(x^2 + 1)) + expression.addMathematicalUnaryFunction("acosh", d -> Math.log(d + (Math.sqrt(Math.pow(d, 2) - 1)))); // Formula: acosh(x) = ln(x + sqrt(x^2 - 1)) + expression.addMathematicalUnaryFunction("atanh", d -> // Formula: atanh(x) = 0.5*ln((1 + x)/(1 - x)) + { + if (Math.abs(d) > 1 || Math.abs(d) == 1) + { + throw new InternalExpressionException("Number must be |x| < 1"); + } + return 0.5 * Math.log((1 + d) / (1 - d)); + }); + expression.addMathematicalUnaryFunction("rad", Math::toRadians); + expression.addMathematicalUnaryFunction("deg", Math::toDegrees); + expression.addMathematicalUnaryFunction("ln", Math::log); + expression.addMathematicalUnaryFunction("ln1p", Math::log1p); + expression.addMathematicalUnaryFunction("log10", Math::log10); + expression.addMathematicalUnaryFunction("log", a -> Math.log(a) / Math.log(2)); + expression.addMathematicalUnaryFunction("log1p", x -> Math.log1p(x) / Math.log(2)); + expression.addMathematicalUnaryFunction("sqrt", Math::sqrt); + expression.addMathematicalUnaryFunction("abs", Math::abs); + expression.addMathematicalUnaryIntFunction("round", Math::round); + expression.addMathematicalUnaryIntFunction("floor", n -> (long) Math.floor(n)); + expression.addMathematicalUnaryIntFunction("ceil", n -> (long) Math.ceil(n)); + + expression.addContextFunction("mandelbrot", 3, (c, t, lv) -> { + double a0 = NumericValue.asNumber(lv.get(0)).getDouble(); + double b0 = NumericValue.asNumber(lv.get(1)).getDouble(); + long maxiter = NumericValue.asNumber(lv.get(2)).getLong(); + double a = 0.0D; + double b = 0.0D; + long iter = 0; + while (a * a + b * b < 4 && iter < maxiter) + { + double temp = a * a - b * b + a0; + b = 2 * a * b + b0; + a = temp; + iter++; + } + long iFinal = iter; + return new NumericValue(iFinal); + }); + + expression.addFunction("max", lv -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'max' requires at least one parameter"); + } + Value max = null; + if (lv.size() == 1 && lv.get(0) instanceof ListValue) + { + lv = ((ListValue) lv.get(0)).getItems(); + } + for (Value parameter : lv) + { + if (max == null || parameter.compareTo(max) > 0) + { + max = parameter; + } + } + return max; + }); + + expression.addFunction("min", lv -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'min' requires at least one parameter"); + } + Value min = null; + if (lv.size() == 1 && lv.get(0) instanceof ListValue) + { + lv = ((ListValue) lv.get(0)).getItems(); + } + for (Value parameter : lv) + { + if (min == null || parameter.compareTo(min) < 0) + { + min = parameter; + } + } + return min; + }); + + expression.addUnaryFunction("relu", v -> v.compareTo(Value.ZERO) < 0 ? Value.ZERO : v); + } +} diff --git a/src/main/java/carpet/script/language/ControlFlow.java b/src/main/java/carpet/script/language/ControlFlow.java new file mode 100644 index 0000000..d96d120 --- /dev/null +++ b/src/main/java/carpet/script/language/ControlFlow.java @@ -0,0 +1,172 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.LazyValue; +import carpet.script.exception.ExitStatement; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ProcessedThrowStatement; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +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 java.util.Map; +import java.util.stream.Collectors; + +public class ControlFlow +{ + public static void apply(Expression expression) // public just to get the javadoc right + { + // needs to be lazy cause of custom contextualization + expression.addLazyBinaryOperator(";", Operators.precedence.get("nextop;"), true, true, t -> Context.Type.VOID, (c, t, lv1, lv2) -> + { + lv1.evalValue(c, Context.VOID); + Value v2 = lv2.evalValue(c, t); + return (cc, tt) -> v2; + }); + + expression.addPureLazyFunction("then", -1, t -> Context.Type.VOID, (c, t, lv) -> { + int imax = lv.size() - 1; + for (int i = 0; i < imax; i++) + { + lv.get(i).evalValue(c, Context.VOID); + } + Value v = lv.get(imax).evalValue(c, t); + return (cc, tt) -> v; + }); + expression.addFunctionalEquivalence(";", "then"); + + + // obvious lazy due to conditional evaluation of arguments + expression.addLazyFunction("if", (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'if' statement needs to have at least one condition and one case"); + } + for (int i = 0; i < lv.size() - 1; i += 2) + { + if (lv.get(i).evalValue(c, Context.BOOLEAN).getBoolean()) + { + Value ret = lv.get(i + 1).evalValue(c, t); + return (cc, tt) -> ret; + } + } + if (lv.size() % 2 == 1) + { + Value ret = lv.get(lv.size() - 1).evalValue(c, t); + return (cc, tt) -> ret; + } + return (cc, tt) -> Value.NULL; + }); + + expression.addImpureFunction("exit", lv -> { + throw new ExitStatement(lv.isEmpty() ? Value.NULL : lv.get(0)); + }); + + expression.addImpureFunction("throw", lv -> + { + switch (lv.size()) + { + case 0 -> throw new ThrowStatement(Value.NULL, Throwables.USER_DEFINED); + case 1 -> throw new ThrowStatement(lv.get(0), Throwables.USER_DEFINED); + case 2 -> throw new ThrowStatement(lv.get(1), Throwables.getTypeForException(lv.get(0).getString())); + case 3 -> throw new ThrowStatement(lv.get(2), Throwables.getTypeForException(lv.get(1).getString()), lv.get(0).getString()); + default -> throw new InternalExpressionException("throw() can't accept more than 3 parameters"); + } + }); + + // needs to be lazy since execution of parameters but first one are conditional + expression.addLazyFunction("try", (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'try' needs at least an expression block, and either a catch_epr, or a number of pairs of filters and catch_expr"); + } + try + { + Value retval = lv.get(0).evalValue(c, t); + return (ct, tt) -> retval; + } + catch (ProcessedThrowStatement ret) + { + if (lv.size() == 1) + { + if (!ret.thrownExceptionType.isUserException()) + { + throw ret; + } + return (ct, tt) -> Value.NULL; + } + if (lv.size() > 3 && lv.size() % 2 == 0) + { + throw new InternalExpressionException("Try-catch block needs the code to run, and either a catch expression for user thrown exceptions, or a number of pairs of filters and catch expressions"); + } + + Value val = null; // This is always assigned at some point, just the compiler doesn't know + + LazyValue defaultVal = c.getVariable("_"); + c.setVariable("_", (ct, tt) -> ret.data.reboundedTo("_")); + LazyValue trace = c.getVariable("_trace"); + c.setVariable("_trace", (ct, tt) -> MapValue.wrap(Map.of( + StringValue.of("stack"), ListValue.wrap(ret.stack.stream().map(f -> ListValue.of( + StringValue.of(f.getModule().name()), + StringValue.of(f.getString()), + NumericValue.of(f.getToken().lineno + 1), + NumericValue.of(f.getToken().linepos + 1) + ))), + + StringValue.of("locals"), MapValue.wrap(ret.context.variables.entrySet().stream().filter(e -> !e.getKey().equals("_trace")).collect(Collectors.toMap( + e -> StringValue.of(e.getKey()), + e -> e.getValue().evalValue(ret.context) + ))), + StringValue.of("token"), ListValue.of( + StringValue.of(ret.token.surface), + NumericValue.of(ret.token.lineno + 1), + NumericValue.of(ret.token.linepos + 1) + ) + ))); + + if (lv.size() == 2) + { + if (ret.thrownExceptionType.isUserException()) + { + val = lv.get(1).evalValue(c, t); + } + } + else + { + int pointer = 1; + while (pointer < lv.size() - 1) + { + if (ret.thrownExceptionType.isRelevantFor(lv.get(pointer).evalValue(c).getString())) + { + val = lv.get(pointer + 1).evalValue(c, t); + break; + } + pointer += 2; + } + } + c.setVariable("_", defaultVal); + if (trace != null) + { + c.setVariable("_trace", trace); + } + else + { + c.delVariable("_trace"); + } + if (val == null) // not handled + { + throw ret; + } + Value retval = val; + return (ct, tt) -> retval; + } + }); + } +} diff --git a/src/main/java/carpet/script/language/DataStructures.java b/src/main/java/carpet/script/language/DataStructures.java new file mode 100644 index 0000000..24adb64 --- /dev/null +++ b/src/main/java/carpet/script/language/DataStructures.java @@ -0,0 +1,391 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.LazyValue; +import carpet.script.api.Auxiliary; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.value.BooleanValue; +import carpet.script.value.ContainerValueInterface; +import carpet.script.value.LContainerValue; +import carpet.script.value.LazyListValue; +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.gson.JsonParseException; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class DataStructures +{ + public static void apply(Expression expression) + { + expression.addFunction("l", lv -> + lv.size() == 1 && lv.get(0) instanceof final LazyListValue llv + ? ListValue.wrap(llv.unroll()) + : new ListValue.ListConstructorValue(lv)); + + expression.addFunction("join", lv -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'join' takes at least 2 arguments"); + } + String delimiter = lv.get(0).getString(); + List toJoin; + if (lv.size() == 2 && lv.get(1) instanceof final LazyListValue llv) + { + toJoin = llv.unroll(); + } + else if (lv.size() == 2 && lv.get(1) instanceof final ListValue llv) + { + toJoin = new ArrayList<>(llv.getItems()); + } + else + { + toJoin = lv.subList(1, lv.size()); + } + return new StringValue(toJoin.stream().map(Value::getString).collect(Collectors.joining(delimiter))); + }); + + expression.addFunction("split", lv -> + { + Value delimiter; + Value hwat; + if (lv.size() == 1) + { + hwat = lv.get(0); + delimiter = null; + } + else if (lv.size() == 2) + { + delimiter = lv.get(0); + hwat = lv.get(1); + } + else + { + throw new InternalExpressionException("'split' takes 1 or 2 arguments"); + } + return hwat.split(delimiter); + }); + + expression.addFunction("slice", lv -> + { + if (lv.size() != 2 && lv.size() != 3) + { + throw new InternalExpressionException("'slice' takes 2 or 3 arguments"); + } + Value hwat = lv.get(0); + long from = NumericValue.asNumber(lv.get(1)).getLong(); + Long to = null; + if (lv.size() == 3) + { + to = NumericValue.asNumber(lv.get(2)).getLong(); + } + return hwat.slice(from, to); + }); + + expression.addFunction("sort", lv -> + { + List toSort = lv; + if (lv.size() == 1 && lv.get(0) instanceof final ListValue llv) + { + toSort = new ArrayList<>(llv.getItems()); + } + Collections.sort(toSort); + return ListValue.wrap(toSort); + }); + + // needs lazy cause sort function is reused + expression.addLazyFunction("sort_key", (c, t, lv) -> //get working with iterators + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("First argument for 'sort_key' should be a List"); + } + Value v = lv.get(0).evalValue(c); + if (!(v instanceof final ListValue list)) + { + throw new InternalExpressionException("First argument for 'sort_key' should be a List"); + } + List toSort = new ArrayList<>(list.getItems()); + if (lv.size() == 1) + { + Collections.shuffle(toSort); + Value ret = ListValue.wrap(toSort); + return (ct, tt) -> ret; + } + LazyValue sortKey = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + toSort.sort((v1, v2) -> { + c.setVariable("_", (cc, tt) -> v1); + Value ev1 = sortKey.evalValue(c); + c.setVariable("_", (cc, tt) -> v2); + Value ev2 = sortKey.evalValue(c); + return ev1.compareTo(ev2); + }); + //revering scope + c.setVariable("_", defaultVal); + Value ret = ListValue.wrap(toSort); + return (cc, tt) -> ret; + }); + + expression.addFunction("range", lv -> + { + NumericValue from = Value.ZERO; + NumericValue to; + NumericValue step = Value.ONE; + int argsize = lv.size(); + if (argsize == 0 || argsize > 3) + { + throw new InternalExpressionException("'range' accepts from 1 to 3 arguments, not " + argsize); + } + to = NumericValue.asNumber(lv.get(0)); + if (lv.size() > 1) + { + from = to; + to = NumericValue.asNumber(lv.get(1)); + if (lv.size() > 2) + { + step = NumericValue.asNumber(lv.get(2)); + } + } + return (from.isInteger() && to.isInteger() && step.isInteger()) + ? LazyListValue.rangeLong(from.getLong(), to.getLong(), step.getLong()) + : LazyListValue.rangeDouble(from.getDouble(), to.getDouble(), step.getDouble()); + }); + + expression.addTypedContextFunction("m", -1, Context.MAPDEF, (c, t, lv) -> + lv.size() == 1 && lv.get(0) instanceof final LazyListValue llv + ? new MapValue(llv.unroll()) + : new MapValue(lv) + ); + + expression.addUnaryFunction("keys", v -> + v instanceof final MapValue map + ? new ListValue(map.getMap().keySet()) + : Value.NULL + ); + + expression.addUnaryFunction("values", v -> + v instanceof final MapValue map + ? new ListValue(map.getMap().values()) + : Value.NULL + ); + + expression.addUnaryFunction("pairs", v -> + v instanceof final MapValue map + ? ListValue.wrap(map.getMap().entrySet().stream().map(p -> ListValue.of(p.getKey(), p.getValue()))) + : Value.NULL); + + expression.addBinaryContextOperator(":", Operators.precedence.get("attribute~:"), true, true, false, (ctx, t, container, address) -> + { + if (container instanceof final LContainerValue lcv) + { + ContainerValueInterface outerContainer = lcv.container(); + if (outerContainer == null) + { + return LContainerValue.NULL_CONTAINER; + } + return outerContainer.get(lcv.address()) instanceof final ContainerValueInterface cvi + ? new LContainerValue(cvi, address) + : LContainerValue.NULL_CONTAINER; + } + if (!(container instanceof final ContainerValueInterface cvi)) + { + return t == Context.LVALUE ? LContainerValue.NULL_CONTAINER : Value.NULL; + } + return t != Context.LVALUE ? cvi.get(address) : new LContainerValue(cvi, address); + }); + + // lazy cause conditional typing - questionable + expression.addLazyFunction("get", (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'get' requires parameters"); + } + if (lv.size() == 1) + { + Value v = lv.get(0).evalValue(c, Context.LVALUE); + if (!(v instanceof final LContainerValue lcv)) + { + return LazyValue.NULL; + } + ContainerValueInterface container = lcv.container(); + if (container == null) + { + return LazyValue.NULL; + } + Value ret = container.get(lcv.address()); + return (cc, tt) -> ret; + } + Value container = lv.get(0).evalValue(c); + for (int i = 1; i < lv.size(); i++) + { + if (!(container instanceof final ContainerValueInterface cvi)) + { + return (cc, tt) -> Value.NULL; + } + container = cvi.get(lv.get(i).evalValue(c)); + } + if (container == null) + { + return (cc, tt) -> Value.NULL; + } + Value finalContainer = container; + return (cc, tt) -> finalContainer; + }); + + // same as `get` + expression.addLazyFunction("has", (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'has' requires parameters"); + } + if (lv.size() == 1) + { + Value v = lv.get(0).evalValue(c, Context.LVALUE); + if (!(v instanceof final LContainerValue lcv)) + { + return LazyValue.NULL; + } + ContainerValueInterface container = lcv.container(); + if (container == null) + { + return LazyValue.NULL; + } + Value ret = BooleanValue.of(container.has(lcv.address())); + return (cc, tt) -> ret; + } + Value container = lv.get(0).evalValue(c); + for (int i = 1; i < lv.size() - 1; i++) + { + if (!(container instanceof final ContainerValueInterface cvi)) + { + return LazyValue.NULL; + } + container = cvi.get(lv.get(i).evalValue(c)); + } + if (!(container instanceof final ContainerValueInterface cvi)) + { + return LazyValue.NULL; + } + Value ret = BooleanValue.of(cvi.has(lv.get(lv.size() - 1).evalValue(c))); + return (cc, tt) -> ret; + }); + + // same as `get` + expression.addLazyFunction("put", (c, t, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'put' takes at least three arguments, a container, address, and values to insert at that index"); + } + Value container = lv.get(0).evalValue(c, Context.LVALUE); + if (container instanceof final LContainerValue lcv) + { + ContainerValueInterface internalContainer = lcv.container(); + if (internalContainer == null) + { + return LazyValue.NULL; + } + Value address = lcv.address(); + Value what = lv.get(1).evalValue(c); + Value retVal = BooleanValue.of((lv.size() > 2) + ? internalContainer.put(address, what, lv.get(2).evalValue(c)) + : internalContainer.put(address, what)); + return (cc, tt) -> retVal; + + } + if (lv.size() < 3) + { + throw new InternalExpressionException("'put' takes at least three arguments, a container, address, and values to insert at that index"); + } + if (!(container instanceof final ContainerValueInterface cvi)) + { + return LazyValue.NULL; + } + Value where = lv.get(1).evalValue(c); + Value what = lv.get(2).evalValue(c); + Value retVal = BooleanValue.of((lv.size() > 3) + ? cvi.put(where, what, lv.get(3).evalValue(c)) + : cvi.put(where, what)); + return (cc, tt) -> retVal; + }); + + // same as `get` + expression.addLazyFunction("delete", (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'delete' requires parameters"); + } + if (lv.size() == 1) + { + Value v = lv.get(0).evalValue(c, Context.LVALUE); + if (!(v instanceof final LContainerValue lcv)) + { + return LazyValue.NULL; + } + ContainerValueInterface container = lcv.container(); + if (container == null) + { + return LazyValue.NULL; + } + Value ret = BooleanValue.of(container.delete(lcv.address())); + return (cc, tt) -> ret; + } + Value container = lv.get(0).evalValue(c); + for (int i = 1; i < lv.size() - 1; i++) + { + if (!(container instanceof final ContainerValueInterface cvi)) + { + return LazyValue.NULL; + } + container = cvi.get(lv.get(i).evalValue(c)); + } + if (!(container instanceof final ContainerValueInterface cvi)) + { + return LazyValue.NULL; + } + Value ret = BooleanValue.of(cvi.delete(lv.get(lv.size() - 1).evalValue(c))); + return (cc, tt) -> ret; + }); + + expression.addUnaryFunction("encode_b64", v -> StringValue.of(Base64.getEncoder().encodeToString(v.getString().getBytes(StandardCharsets.UTF_8)))); + expression.addUnaryFunction("decode_b64", v -> { + try + { + return StringValue.of(new String(Base64.getDecoder().decode(v.getString()), StandardCharsets.UTF_8)); + } + catch (IllegalArgumentException iae) + { + throw new ThrowStatement("Invalid b64 string: " + v.getString(), Throwables.B64_ERROR); + } + }); + + expression.addUnaryFunction("encode_json", v -> StringValue.of(v.toJson().toString())); + expression.addUnaryFunction("decode_json", v -> { + try + { + return Auxiliary.GSON.fromJson(v.getString(), Value.class); + } + catch (JsonParseException jpe) + { + throw new ThrowStatement("Invalid json string: " + v.getString(), Throwables.JSON_ERROR); + } + }); + } +} diff --git a/src/main/java/carpet/script/language/Functions.java b/src/main/java/carpet/script/language/Functions.java new file mode 100644 index 0000000..8bc2971 --- /dev/null +++ b/src/main/java/carpet/script/language/Functions.java @@ -0,0 +1,149 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Fluff; +import carpet.script.LazyValue; +import carpet.script.Tokenizer; +import carpet.script.argument.FunctionArgument; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ReturnStatement; +import carpet.script.value.FunctionSignatureValue; +import carpet.script.value.FunctionValue; +import carpet.script.value.FunctionAnnotationValue; +import carpet.script.value.ListValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class Functions +{ + public static void apply(Expression expression) // public just to get the javadoc right + { + // artificial construct to handle user defined functions and function definitions + expression.addContextFunction("import", -1, (c, t, lv) -> + { + if (lv.size() < 1) + { + throw new InternalExpressionException("'import' needs at least a module name to import, and list of values to import"); + } + String moduleName = lv.get(0).getString(); + c.host.importModule(c, moduleName); + moduleName = moduleName.toLowerCase(Locale.ROOT); + if (lv.size() > 1) + { + c.host.importNames(c, expression.module, moduleName, lv.subList(1, lv.size()).stream().map(Value::getString).toList()); + } + return t == Context.VOID ? Value.NULL : ListValue.wrap(c.host.availableImports(moduleName).map(StringValue::new)); + }); + + + // needs to be lazy because of custom context of execution of arguments as a signature + expression.addCustomFunction("call", new Fluff.AbstractLazyFunction(-1, "call") + { + @Override + public LazyValue lazyEval(Context c, Context.Type t, Expression expr, Tokenizer.Token tok, List lv) + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'call' expects at least function name to call"); + } + //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 + { + List args = Fluff.AbstractFunction.unpackLazy(lv, c, Context.NONE); + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, args, 0, false, true); + FunctionValue fun = functionArgument.function; + return fun.callInContext(c, t, functionArgument.args); + } + // gimme signature + String name = lv.get(0).evalValue(c, Context.NONE).getString(); + List args = new ArrayList<>(); + List globals = new ArrayList<>(); + String varArgs = null; + for (int i = 1; i < lv.size(); i++) + { + Value v = lv.get(i).evalValue(c, Context.LOCALIZATION); + if (!v.isBound()) + { + throw new InternalExpressionException("Only variables can be used in function signature, not " + v.getString()); + } + if (v instanceof final FunctionAnnotationValue fav) + { + if (fav.type == FunctionAnnotationValue.Type.GLOBAL) + { + globals.add(v.boundVariable); + } + else + { + if (varArgs != null) + { + throw new InternalExpressionException("Variable argument identifier is already defined as " + varArgs + ", trying to overwrite with " + v.boundVariable); + } + varArgs = v.boundVariable; + } + } + else + { + args.add(v.boundVariable); + } + } + Value retval = new FunctionSignatureValue(name, args, varArgs, globals); + return (cc, tt) -> retval; + } + + @Override + public boolean pure() + { + return false; //true for sinature, but lets leave it for later + } + + @Override + public boolean transitive() + { + return false; + } + + @Override + public Context.Type staticType(Context.Type outerType) + { + return outerType == Context.SIGNATURE ? Context.LOCALIZATION : Context.NONE; + } + }); + + + expression.addContextFunction("outer", 1, (c, t, lv) -> + { + if (t != Context.LOCALIZATION) + { + throw new InternalExpressionException("Outer scoping of variables is only possible in function signatures."); + } + return new FunctionAnnotationValue(lv.get(0), FunctionAnnotationValue.Type.GLOBAL); + }); + + //assigns const procedure to the lhs, returning its previous value + // must be lazy due to RHS being an expression to save to execute + expression.addLazyBinaryOperatorWithDelegation("->", Operators.precedence.get("def->"), false, false, (c, type, e, t, lv1, lv2) -> + { + if (type == Context.MAPDEF) + { + Value result = ListValue.of(lv1.evalValue(c), lv2.evalValue(c)); + return (cc, tt) -> result; + } + Value v1 = lv1.evalValue(c, Context.SIGNATURE); + if (!(v1 instanceof final FunctionSignatureValue sign)) + { + throw new InternalExpressionException("'->' operator requires a function signature on the LHS"); + } + Value result = expression.createUserDefinedFunction(c, sign.identifier(), e, t, sign.arguments(), sign.varArgs(), sign.globals(), lv2); + return (cc, tt) -> result; + }); + + expression.addImpureFunction("return", lv -> { + throw new ReturnStatement(lv.size() == 0 ? Value.NULL : lv.get(0)); + }); + } +} diff --git a/src/main/java/carpet/script/language/Loops.java b/src/main/java/carpet/script/language/Loops.java new file mode 100644 index 0000000..552325d --- /dev/null +++ b/src/main/java/carpet/script/language/Loops.java @@ -0,0 +1,542 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.LazyValue; +import carpet.script.exception.BreakStatement; +import carpet.script.exception.ContinueStatement; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.AbstractListValue; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Loops +{ + public static void apply(Expression expression) + { + // condition and expression will get a bound '_i' + // returns last successful expression or false + // while(cond, limit, expr) => ?? + expression.addImpureFunction("break", lv -> + { + if (lv.isEmpty()) + { + throw new BreakStatement(null); + } + if (lv.size() == 1) + { + throw new BreakStatement(lv.get(0)); + } + throw new InternalExpressionException("'break' can only be called with zero or one argument"); + }); + + expression.addImpureFunction("continue", lv -> + { + if (lv.isEmpty()) + { + throw new ContinueStatement(null); + } + if (lv.size() == 1) + { + throw new ContinueStatement(lv.get(0)); + } + throw new InternalExpressionException("'continue' can only be called with zero or one argument"); + }); + + // lazy + expression.addLazyFunction("while", -1, (c, t, lv) -> + { + if (lv.size() == 2) { // lets do nasty way so performance is not affected (might be super unnecessary, but hey) + LazyValue condition = lv.get(0); + LazyValue expr = lv.get(1); + long i = 0; + Value lastOne = Value.NULL; + //scoping + LazyValue defaultVal = c.getVariable("_"); + c.setVariable("_", (cc, tt) -> new NumericValue(0).bindTo("_")); + while (condition.evalValue(c, Context.BOOLEAN).getBoolean()) + { + try + { + lastOne = expr.evalValue(c, t); + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null) + { + lastOne = stmt.retval; + } + if (stmt instanceof BreakStatement) + { + break; + } + } + i++; + long seriously = i; + c.setVariable("_", (cc, tt) -> new NumericValue(seriously).bindTo("_")); + } + //revering scope + c.setVariable("_", defaultVal); + Value lastValueNoKidding = lastOne; + return (cc, tt) -> lastValueNoKidding; + } + long limit = NumericValue.asNumber(lv.get(1).evalValue(c)).getLong(); + LazyValue condition = lv.get(0); + LazyValue expr = lv.get(2); + long i = 0; + Value lastOne = Value.NULL; + //scoping + LazyValue defaultVal = c.getVariable("_"); + c.setVariable("_", (cc, tt) -> new NumericValue(0).bindTo("_")); + while (i < limit && condition.evalValue(c, Context.BOOLEAN).getBoolean()) + { + try + { + lastOne = expr.evalValue(c, t); + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null) + { + lastOne = stmt.retval; + } + if (stmt instanceof BreakStatement) + { + break; + } + } + i++; + long seriously = i; + c.setVariable("_", (cc, tt) -> new NumericValue(seriously).bindTo("_")); + } + //revering scope + c.setVariable("_", defaultVal); + Value lastValueNoKidding = lastOne; + return (cc, tt) -> lastValueNoKidding; + }); + + // loop(Num, expr) => lastdefaultValue + // expr receives bounded variable '_' indicating iteration + expression.addLazyFunction("loop", 2, (c, t, lv) -> + { + long limit = NumericValue.asNumber(lv.get(0).evalValue(c, Context.NONE)).getLong(); + Value lastOne = Value.NULL; + LazyValue expr = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + for (long i = 0; i < limit; i++) + { + long whyYouAsk = i; + c.setVariable("_", (cc, tt) -> new NumericValue(whyYouAsk).bindTo("_")); + try + { + lastOne = expr.evalValue(c, t); + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null) + { + lastOne = stmt.retval; + } + if (stmt instanceof BreakStatement) + { + break; + } + } + } + //revering scope + c.setVariable("_", defaultVal); + Value trulyLastOne = lastOne; + return (cc, tt) -> trulyLastOne; + }); + + // map(list or Num, expr) => list_results + // receives bounded variable '_' with the expression + expression.addLazyFunction("map", 2, (c, t, lv) -> + { + Value rval = lv.get(0).evalValue(c, Context.NONE); + if (rval.isNull()) + { + return ListValue.lazyEmpty(); + } + if (!(rval instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("First argument of 'map' function should be a list or iterator"); + } + Iterator iterator = alv.iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + LazyValue iterVal = c.getVariable("_i"); + List result = new ArrayList<>(); + for (int i = 0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + if(next == Value.EOL) { + continue; + } + String variable = next.boundVariable; + next.bindTo("_"); + int doYouReally = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(doYouReally).bindTo("_i")); + try + { + result.add(expr.evalValue(c, t)); + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null) + { + result.add(stmt.retval); + } + if (stmt instanceof BreakStatement) + { + next.boundVariable = variable; + break; + } + } + next.boundVariable = variable; + } + ((AbstractListValue) rval).fatality(); + Value ret = ListValue.wrap(result); + //revering scope + c.setVariable("_", defaultVal); + c.setVariable("_i", iterVal); + return (cc, tt) -> ret; + }); + + // grep(list or num, expr) => list + // receives bounded variable '_' with the expression, and "_i" with index + // produces list of values for which the expression is true + expression.addLazyFunction("filter", 2, (c, t, lv) -> + { + Value rval = lv.get(0).evalValue(c, Context.NONE); + if (rval.isNull()) + { + return ListValue.lazyEmpty(); + } + if (!(rval instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("First argument of 'filter' function should be a list or iterator"); + } + Iterator iterator = alv.iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + LazyValue iterVal = c.getVariable("_i"); + List result = new ArrayList<>(); + for (int i = 0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + if(next == Value.EOL) { + continue; + } + String veriable = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + try + { + if (expr.evalValue(c, Context.BOOLEAN).getBoolean()) + { + result.add(next); + } + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null && stmt.retval.getBoolean()) + { + result.add(next); + } + if (stmt instanceof BreakStatement) + { + next.boundVariable = veriable; + break; + } + } + next.boundVariable = veriable; + } + ((AbstractListValue) rval).fatality(); + Value ret = ListValue.wrap(result); + //revering scope + c.setVariable("_", defaultVal); + c.setVariable("_i", iterVal); + return (cc, tt) -> ret; + }); + + // first(list, expr) => elem or null + // receives bounded variable '_' with the expression, and "_i" with index + // returns first element on the list for which the expr is true + expression.addLazyFunction("first", 2, (c, t, lv) -> + { + Value rval = lv.get(0).evalValue(c, Context.NONE); + if (rval.isNull()) + { + return LazyValue.NULL; + } + if (!(rval instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("First argument of 'first' function should be a list or iterator"); + } + Iterator iterator = alv.iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + LazyValue iterVal = c.getVariable("_i"); + Value result = Value.NULL; + for (int i = 0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + if(next == Value.EOL) { + continue; + } + String variable = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + try + { + if (expr.evalValue(c, Context.BOOLEAN).getBoolean()) + { + result = next; + next.boundVariable = variable; + break; + } + } + catch (BreakStatement stmt) + { + result = stmt.retval == null ? next : stmt.retval; + next.boundVariable = variable; + break; + } + catch (ContinueStatement ignored) + { + throw new InternalExpressionException("'continue' inside 'first' function has no sense"); + } + next.boundVariable = variable; + } + //revering scope + ((AbstractListValue) rval).fatality(); + Value whyWontYouTrustMeJava = result; + c.setVariable("_", defaultVal); + c.setVariable("_i", iterVal); + return (cc, tt) -> whyWontYouTrustMeJava; + }); + + // all(list, expr) => boolean + // receives bounded variable '_' with the expression, and "_i" with index + // returns true if expr is true for all items + expression.addLazyFunction("all", 2, (c, t, lv) -> + { + Value rval = lv.get(0).evalValue(c, Context.NONE); + if (rval.isNull()) + { + return LazyValue.TRUE; + } + if (!(rval instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("First argument of 'all' function should be a list or iterator"); + } + Iterator iterator = alv.iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + LazyValue iterVal = c.getVariable("_i"); + LazyValue result = LazyValue.TRUE; + for (int i = 0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + if(next == Value.EOL) { + continue; + } + String variable = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + if (!expr.evalValue(c, Context.BOOLEAN).getBoolean()) + { + result = LazyValue.FALSE; + next.boundVariable = variable; + break; + } + next.boundVariable = variable; + } + //revering scope + ((AbstractListValue) rval).fatality(); + c.setVariable("_", defaultVal); + c.setVariable("_i", iterVal); + return result; + }); + + // runs traditional for(init, condition, increment, body) tri-argument for loop with body in between + expression.addLazyFunction("c_for", 4, (c, t, lv) -> + { + LazyValue initial = lv.get(0); + LazyValue condition = lv.get(1); + LazyValue increment = lv.get(2); + LazyValue body = lv.get(3); + int iterations = 0; + for (initial.evalValue(c, Context.VOID); condition.evalValue(c, Context.BOOLEAN).getBoolean(); increment.evalValue(c, Context.VOID)) + { + try + { + body.evalValue(c, Context.VOID); + } + catch (BreakStatement stmt) + { + break; + } + catch (ContinueStatement ignored) + { + } + iterations++; + } + int finalIterations = iterations; + return (cc, tt) -> new NumericValue(finalIterations); + }); + + // similar to map, but returns total number of successes + // for(list, expr) => success_count + // can be substituted for first and all, but first is more efficient and all doesn't require knowing list size + expression.addLazyFunction("for", 2, (c, t, lv) -> + { + Value rval = lv.get(0).evalValue(c, Context.NONE); + if (rval.isNull()) + { + return LazyValue.ZERO; + } + if (!(rval instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("First argument of 'for' function should be a list or iterator"); + } + Iterator iterator = alv.iterator(); + LazyValue expr = lv.get(1); + //scoping + LazyValue defaultVal = c.getVariable("_"); + LazyValue iterVal = c.getVariable("_i"); + int successCount = 0; + for (int i = 0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + if(next == Value.EOL) { + continue; + } + String variable = next.boundVariable; + next.bindTo("_"); + int seriously = i; + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + Value result = Value.FALSE; + try + { + result = expr.evalValue(c, t); + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null) + { + result = stmt.retval; + } + if (stmt instanceof BreakStatement) + { + next.boundVariable = variable; + break; + } + } + if (t != Context.VOID && result.getBoolean()) + { + successCount++; + } + next.boundVariable = variable; + } + //revering scope + ((AbstractListValue) rval).fatality(); + c.setVariable("_", defaultVal); + c.setVariable("_i", iterVal); + long promiseWontChange = successCount; + return (cc, tt) -> new NumericValue(promiseWontChange); + }); + + + // reduce(list, expr, ?acc) => value + // reduces values in the list with expression that gets accumulator + // each iteration expr receives acc - accumulator, and '_' - current list value + // returned value is substituted to the accumulator + expression.addLazyFunction("reduce", 3, (c, t, lv) -> + { + + Value rval = lv.get(0).evalValue(c, Context.NONE); + if (rval.isNull()) + { + return ListValue.lazyEmpty(); + } + if (!(rval instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("First argument of 'reduce' should be a list or iterator"); + } + LazyValue expr = lv.get(1); + Value acc = lv.get(2).evalValue(c, Context.NONE); + Iterator iterator = alv.iterator(); + + if (!iterator.hasNext()) + { + Value seriouslyWontChange = acc; + return (cc, tt) -> seriouslyWontChange; + } + + //scoping + LazyValue defaultVal = c.getVariable("_"); + LazyValue accumulatorVal = c.getVariable("_a"); + LazyValue iterVal = c.getVariable("_i"); + + for (int i = 0; iterator.hasNext(); i++) + { + Value next = iterator.next(); + if(next == Value.EOL) { + continue; + } + String variable = next.boundVariable; + next.bindTo("_"); + Value promiseWontChangeYou = acc; + int seriously = i; + c.setVariable("_a", (cc, tt) -> promiseWontChangeYou.bindTo("_a")); + c.setVariable("_", (cc, tt) -> next); + c.setVariable("_i", (cc, tt) -> new NumericValue(seriously).bindTo("_i")); + try + { + acc = expr.evalValue(c, t); + } + catch (BreakStatement | ContinueStatement stmt) + { + if (stmt.retval != null) + { + acc = stmt.retval; + } + if (stmt instanceof BreakStatement) + { + next.boundVariable = variable; + break; + } + } + next.boundVariable = variable; + } + //reverting scope + ((AbstractListValue) rval).fatality(); + c.setVariable("_a", accumulatorVal); + c.setVariable("_", defaultVal); + c.setVariable("_i", iterVal); + + Value hopeItsEnoughPromise = acc; + return (cc, tt) -> hopeItsEnoughPromise; + }); + } +} diff --git a/src/main/java/carpet/script/language/Operators.java b/src/main/java/carpet/script/language/Operators.java new file mode 100644 index 0000000..a1c2d1a --- /dev/null +++ b/src/main/java/carpet/script/language/Operators.java @@ -0,0 +1,568 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.LazyValue; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.AbstractListValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.ContainerValueInterface; +import carpet.script.value.FunctionAnnotationValue; +import carpet.script.value.FunctionUnpackedArgumentsValue; +import carpet.script.value.LContainerValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NumericValue; +import carpet.script.value.Value; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class Operators +{ + public static final Map precedence = new HashMap<>() + {{ + put("attribute~:", 80); + put("unary+-!...", 60); + put("exponent^", 40); + put("multiplication*/%", 30); + put("addition+-", 20); + put("compare>=><=<", 10); + put("equal==!=", 7); + put("and&&", 5); + put("or||", 4); + put("assign=<>", 3); + put("def->", 2); + put("nextop;", 1); + }}; + + public static void apply(Expression expression) + { + expression.addBinaryOperator("+", precedence.get("addition+-"), true, Value::add); + expression.addFunction("sum", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + Value accumulator = lv.get(0); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator.add(v); + } + return accumulator; + }); + expression.addFunctionalEquivalence("+", "sum"); + + expression.addBinaryOperator("-", precedence.get("addition+-"), true, Value::subtract); + expression.addFunction("difference", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + Value accumulator = lv.get(0); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator.subtract(v); + } + return accumulator; + }); + expression.addFunctionalEquivalence("-", "difference"); + + expression.addBinaryOperator("*", precedence.get("multiplication*/%"), true, Value::multiply); + expression.addFunction("product", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + Value accumulator = lv.get(0); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator.multiply(v); + } + return accumulator; + }); + expression.addFunctionalEquivalence("*", "product"); + + expression.addBinaryOperator("/", precedence.get("multiplication*/%"), true, Value::divide); + expression.addFunction("quotient", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + Value accumulator = lv.get(0); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator.divide(v); + } + return accumulator; + }); + expression.addFunctionalEquivalence("/", "quotient"); + + expression.addBinaryOperator("%", precedence.get("multiplication*/%"), true, (v1, v2) -> + NumericValue.asNumber(v1).mod(NumericValue.asNumber(v2))); + expression.addBinaryOperator("^", precedence.get("exponent^"), false, (v1, v2) -> + new NumericValue(java.lang.Math.pow(NumericValue.asNumber(v1).getDouble(), NumericValue.asNumber(v2).getDouble()))); + + expression.addFunction("bitwise_and", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + long accumulator = NumericValue.asNumber(lv.get(0)).getLong(); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator & NumericValue.asNumber(v).getLong(); + } + return new NumericValue(accumulator); + }); + + expression.addFunction("bitwise_xor", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + long accumulator = NumericValue.asNumber(lv.get(0)).getLong(); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator ^ NumericValue.asNumber(v).getLong(); + } + return new NumericValue(accumulator); + }); + + expression.addFunction("bitwise_or", lv -> { + int size = lv.size(); + if (size == 0) + { + return Value.NULL; + } + long accumulator = NumericValue.asNumber(lv.get(0)).getLong(); + for (Value v : lv.subList(1, size)) + { + accumulator = accumulator | NumericValue.asNumber(v).getLong(); + } + return new NumericValue(accumulator); + }); + + // lazy cause RHS is only conditional + expression.addLazyBinaryOperator("&&", precedence.get("and&&"), false, true, t -> Context.Type.BOOLEAN, (c, t, lv1, lv2) -> + { // todo check how is optimizations going + Value v1 = lv1.evalValue(c, Context.BOOLEAN); + return v1.getBoolean() ? lv2 : ((cc, tt) -> v1); + }); + + expression.addPureLazyFunction("and", -1, t -> Context.Type.BOOLEAN, (c, t, lv) -> { + int last = lv.size() - 1; + if (last == -1) + { + return LazyValue.TRUE; + } + for (LazyValue l : lv.subList(0, last)) + { + Value val = l.evalValue(c, Context.Type.BOOLEAN); + if (val instanceof final FunctionUnpackedArgumentsValue fuav) + { + for (Value it : fuav) + { + if (!it.getBoolean()) + { + return (cc, tt) -> it; + } + } + } + else + { + if (!val.getBoolean()) + { + return (cc, tt) -> val; + } + } + } + return lv.get(last); + }); + expression.addFunctionalEquivalence("&&", "and"); + + // lazy cause RHS is only conditional + expression.addLazyBinaryOperator("||", precedence.get("or||"), false, true, t -> Context.Type.BOOLEAN, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.BOOLEAN); + return v1.getBoolean() ? ((cc, tt) -> v1) : lv2; + }); + + expression.addPureLazyFunction("or", -1, t -> Context.Type.BOOLEAN, (c, t, lv) -> { + int last = lv.size() - 1; + if (last == -1) + { + return LazyValue.FALSE; + } + for (LazyValue l : lv.subList(0, last)) + { + Value val = l.evalValue(c, Context.Type.BOOLEAN); + if (val instanceof final FunctionUnpackedArgumentsValue fuav) + { + for (Value it : fuav) + { + if (it.getBoolean()) + { + return (cc, tt) -> it; + } + } + } + else + { + if (val.getBoolean()) + { + return (cc, tt) -> val; + } + } + } + return lv.get(last); + }); + expression.addFunctionalEquivalence("||", "or"); + + expression.addBinaryOperator("~", precedence.get("attribute~:"), true, Value::in); + + expression.addBinaryOperator(">", precedence.get("compare>=><=<"), false, (v1, v2) -> + BooleanValue.of(v1.compareTo(v2) > 0)); + expression.addFunction("decreasing", lv -> { + int size = lv.size(); + if (size < 2) + { + return Value.TRUE; + } + Value prev = lv.get(0); + for (Value next : lv.subList(1, size)) + { + if (prev.compareTo(next) <= 0) + { + return Value.FALSE; + } + prev = next; + } + return Value.TRUE; + }); + expression.addFunctionalEquivalence(">", "decreasing"); + + expression.addBinaryOperator(">=", precedence.get("compare>=><=<"), false, (v1, v2) -> + BooleanValue.of(v1.compareTo(v2) >= 0)); + expression.addFunction("nonincreasing", lv -> { + int size = lv.size(); + if (size < 2) + { + return Value.TRUE; + } + Value prev = lv.get(0); + for (Value next : lv.subList(1, size)) + { + if (prev.compareTo(next) < 0) + { + return Value.FALSE; + } + prev = next; + } + return Value.TRUE; + }); + expression.addFunctionalEquivalence(">=", "nonincreasing"); + + expression.addBinaryOperator("<", precedence.get("compare>=><=<"), false, (v1, v2) -> + BooleanValue.of(v1.compareTo(v2) < 0)); + expression.addFunction("increasing", lv -> { + int size = lv.size(); + if (size < 2) + { + return Value.TRUE; + } + Value prev = lv.get(0); + for (Value next : lv.subList(1, size)) + { + if (prev.compareTo(next) >= 0) + { + return Value.FALSE; + } + prev = next; + } + return Value.TRUE; + }); + expression.addFunctionalEquivalence("<", "increasing"); + + expression.addBinaryOperator("<=", precedence.get("compare>=><=<"), false, (v1, v2) -> + BooleanValue.of(v1.compareTo(v2) <= 0)); + expression.addFunction("nondecreasing", lv -> { + int size = lv.size(); + if (size < 2) + { + return Value.TRUE; + } + Value prev = lv.get(0); + for (Value next : lv.subList(1, size)) + { + if (prev.compareTo(next) > 0) + { + return Value.FALSE; + } + prev = next; + } + return Value.TRUE; + }); + expression.addFunctionalEquivalence("<=", "nondecreasing"); + expression.addMathematicalBinaryIntFunction("bitwise_shift_left", (num, amount) -> num << amount); + expression.addMathematicalBinaryIntFunction("bitwise_shift_right", (num, amount) -> num >>> amount); + expression.addMathematicalBinaryIntFunction("bitwise_arithmetic_shift_right", (num, amount) -> num >> amount); + expression.addMathematicalBinaryIntFunction("bitwise_roll_left", (num, amount) -> Long.rotateLeft(num, (int)amount)); + expression.addMathematicalBinaryIntFunction("bitwise_roll_right", (num, amount) -> Long.rotateRight(num, (int)amount)); + expression.addMathematicalUnaryIntFunction("bitwise_not", d -> { + long num = (long) d; + return ~num; + }); + expression.addMathematicalUnaryIntFunction("bitwise_popcount", d -> (long) Long.bitCount((long)d)); + expression.addMathematicalUnaryIntFunction("double_to_long_bits", Double::doubleToLongBits); + expression.addUnaryFunction("long_to_double_bits", v -> + new NumericValue(Double.longBitsToDouble(NumericValue.asNumber(v).getLong()))); + expression.addBinaryOperator("==", precedence.get("equal==!="), false, (v1, v2) -> + v1.equals(v2) ? Value.TRUE : Value.FALSE); + expression.addFunction("equal", lv -> { + int size = lv.size(); + if (size < 2) + { + return Value.TRUE; + } + Value prev = lv.get(0); + for (Value next : lv.subList(1, size)) + { + if (!prev.equals(next)) + { + return Value.FALSE; + } + prev = next; + } + return Value.TRUE; + }); + expression.addFunctionalEquivalence("==", "equal"); + expression.addBinaryOperator("!=", precedence.get("equal==!="), false, (v1, v2) -> + v1.equals(v2) ? Value.FALSE : Value.TRUE); + expression.addFunction("unique", lv -> { + int size = lv.size(); + if (size < 2) + { + return Value.TRUE; + } + // need to order them so same obejects will be next to each other. + lv.sort(Comparator.comparingInt(Value::hashCode)); + Value prev = lv.get(0); + for (Value next : lv.subList(1, size)) + { + if (prev.equals(next)) + { + return Value.FALSE; + } + prev = next; + } + return Value.TRUE; + }); + expression.addFunctionalEquivalence("!=", "unique"); + + // lazy cause of assignment which is non-trivial + expression.addLazyBinaryOperator("=", precedence.get("assign=<>"), false, false, t -> Context.Type.LVALUE, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.LVALUE); + Value v2 = lv2.evalValue(c); + if (v1 instanceof final ListValue.ListConstructorValue lcv && v2 instanceof final ListValue list) + { + List ll = lcv.getItems(); + List rl = list.getItems(); + if (ll.size() < rl.size()) + { + throw new InternalExpressionException("Too many values to unpack"); + } + if (ll.size() > rl.size()) + { + throw new InternalExpressionException("Too few values to unpack"); + } + for (Value v : ll) + { + v.assertAssignable(); + } + Iterator li = ll.iterator(); + Iterator ri = rl.iterator(); + while (li.hasNext()) + { + String lname = li.next().getVariable(); + Value vval = ri.next().reboundedTo(lname); + expression.setAnyVariable(c, lname, (cc, tt) -> vval); + } + return (cc, tt) -> Value.TRUE; + } + if (v1 instanceof final LContainerValue lcv) + { + ContainerValueInterface container = lcv.container(); + if (container == null) + { + return (cc, tt) -> Value.NULL; + } + Value address = lcv.address(); + if (!(container.put(address, v2))) + { + return (cc, tt) -> Value.NULL; + } + return (cc, tt) -> v2; + } + v1.assertAssignable(); + String varname = v1.getVariable(); + Value copy = v2.reboundedTo(varname); + LazyValue boundedLHS = (cc, tt) -> copy; + expression.setAnyVariable(c, varname, boundedLHS); + return boundedLHS; + }); + + // lazy due to assignment + expression.addLazyBinaryOperator("+=", precedence.get("assign=<>"), false, false, t -> Context.Type.LVALUE, (c, t, lv1, lv2) -> + { + Value v1 = lv1.evalValue(c, Context.LVALUE); + Value v2 = lv2.evalValue(c); + if (v1 instanceof final ListValue.ListConstructorValue lcv && v2 instanceof final ListValue list) + { + List ll = lcv.getItems(); + List rl = list.getItems(); + if (ll.size() < rl.size()) + { + throw new InternalExpressionException("Too many values to unpack"); + } + if (ll.size() > rl.size()) + { + throw new InternalExpressionException("Too few values to unpack"); + } + for (Value v : ll) + { + v.assertAssignable(); + } + Iterator li = ll.iterator(); + Iterator ri = rl.iterator(); + while (li.hasNext()) + { + Value lval = li.next(); + String lname = lval.getVariable(); + Value result = lval.add(ri.next()).bindTo(lname); + expression.setAnyVariable(c, lname, (cc, tt) -> result); + } + return (cc, tt) -> Value.TRUE; + } + if (v1 instanceof final LContainerValue lcv) + { + ContainerValueInterface cvi = lcv.container(); + if (cvi == null) + { + throw new InternalExpressionException("Failed to resolve left hand side of the += operation"); + } + Value key = lcv.address(); + 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(); + LazyValue boundedLHS; + if (v1 instanceof ListValue || v1 instanceof MapValue) + { + ((AbstractListValue) v1).append(v2); + boundedLHS = (cc, tt) -> v1; + } + else + { + Value result = v1.add(v2).bindTo(varname); + boundedLHS = (cc, tt) -> result; + } + expression.setAnyVariable(c, varname, boundedLHS); + return boundedLHS; + }); + + expression.addBinaryContextOperator("<>", precedence.get("assign=<>"), false, false, false, (c, t, v1, v2) -> + { + if (v1 instanceof final ListValue.ListConstructorValue lcv1 && v2 instanceof final ListValue.ListConstructorValue lcv2) + { + List ll = lcv1.getItems(); + List rl = lcv2.getItems(); + if (ll.size() < rl.size()) + { + throw new InternalExpressionException("Too many values to unpack"); + } + if (ll.size() > rl.size()) + { + throw new InternalExpressionException("Too few values to unpack"); + } + for (Value v : ll) + { + v.assertAssignable(); + } + for (Value v : rl) + { + v.assertAssignable(); + } + Iterator li = ll.iterator(); + Iterator ri = rl.iterator(); + while (li.hasNext()) + { + Value lval = li.next(); + Value rval = ri.next(); + String lname = lval.getVariable(); + String rname = rval.getVariable(); + lval.reboundedTo(rname); + rval.reboundedTo(lname); + expression.setAnyVariable(c, lname, (cc, tt) -> rval); + expression.setAnyVariable(c, rname, (cc, tt) -> lval); + } + return Value.TRUE; + } + v1.assertAssignable(); + v2.assertAssignable(); + String lvalvar = v1.getVariable(); + String rvalvar = v2.getVariable(); + Value lval = v2.reboundedTo(lvalvar); + Value rval = v1.reboundedTo(rvalvar); + expression.setAnyVariable(c, lvalvar, (cc, tt) -> lval); + expression.setAnyVariable(c, rvalvar, (cc, tt) -> rval); + return lval; + }); + + expression.addUnaryOperator("-", false, v -> NumericValue.asNumber(v).opposite()); + + expression.addUnaryOperator("+", false, NumericValue::asNumber); + + // could be non-lazy, but who cares - its a small one. + expression.addLazyUnaryOperator("!", precedence.get("unary+-!..."), false, true, x -> Context.Type.BOOLEAN, (c, t, lv) -> + lv.evalValue(c, Context.BOOLEAN).getBoolean() ? (cc, tt) -> Value.FALSE : (cc, tt) -> Value.TRUE + ); // might need context boolean + + // lazy because of typed evaluation of the argument + expression.addLazyUnaryOperator("...", Operators.precedence.get("unary+-!..."), false, true, t -> t == Context.Type.LOCALIZATION ? Context.NONE : t, (c, t, lv) -> + { + if (t == Context.LOCALIZATION) + { + return (cc, tt) -> new FunctionAnnotationValue(lv.evalValue(c), FunctionAnnotationValue.Type.VARARG); + } + if (!(lv.evalValue(c, t) instanceof final AbstractListValue alv)) + { + throw new InternalExpressionException("Unable to unpack a non-list"); + } + FunctionUnpackedArgumentsValue fuaval = new FunctionUnpackedArgumentsValue(alv.unpack()); + return (cc, tt) -> fuaval; + }); + + } +} diff --git a/src/main/java/carpet/script/language/Sys.java b/src/main/java/carpet/script/language/Sys.java new file mode 100644 index 0000000..31d13c7 --- /dev/null +++ b/src/main/java/carpet/script/language/Sys.java @@ -0,0 +1,525 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.LazyValue; +import carpet.script.exception.InternalExpressionException; +import carpet.script.utils.PerlinNoiseSampler; +import carpet.script.utils.SimplexNoiseSampler; +import carpet.script.value.BooleanValue; +import carpet.script.value.FunctionValue; +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 java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.IllegalFormatException; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class Sys +{ + public static final Random randomizer = new Random(); + // %[argument_index$][flags][width][.precision][t]conversion + private static final Pattern formatPattern = Pattern.compile("%(\\d+\\$)?([-#+ 0,(<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"); + + public static void apply(Expression expression) + { + expression.addUnaryFunction("hash_code", v -> new NumericValue(v.hashCode())); + + expression.addImpureUnaryFunction("copy", Value::deepcopy); + + expression.addTypedContextFunction("bool", 1, Context.BOOLEAN, (c, t, lv) -> + { + Value v = lv.get(0); + if (v instanceof StringValue) + { + String str = v.getString().toLowerCase(Locale.ROOT); + if ("false".equals(str) || "null".equals(str)) + { + return Value.FALSE; + } + } + return BooleanValue.of(v.getBoolean()); + }); + + expression.addUnaryFunction("number", v -> + { + if (v instanceof final NumericValue num) + { + return num.clone(); + } + if (v instanceof ListValue || v instanceof MapValue) + { + return new NumericValue(v.length()); + } + try + { + return new NumericValue(v.getString()); + } + catch (NumberFormatException format) + { + return Value.NULL; + } + }); + + expression.addFunction("str", lv -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'str' requires at least one argument"); + } + String format = lv.get(0).getString(); + if (lv.size() == 1) + { + return new StringValue(format); + } + int argIndex = 1; + if (lv.get(1) instanceof final ListValue list && lv.size() == 2) + { + lv = list.getItems(); + argIndex = 0; + } + List args = new ArrayList<>(); + Matcher m = formatPattern.matcher(format); + + for (int i = 0, len = format.length(); i < len; ) + { + if (m.find(i)) + { + // Anything between the start of the string and the beginning + // of the format specifier is either fixed text or contains + // an invalid format string. + // [[scarpet]] but we skip it and let the String.format fail + char fmt = m.group(6).toLowerCase().charAt(0); + if (fmt == 's') + { + if (argIndex >= lv.size()) + { + throw new InternalExpressionException("Not enough arguments for " + m.group(0)); + } + args.add(lv.get(argIndex).getString()); + argIndex++; + } + else if (fmt == 'd' || fmt == 'o' || fmt == 'x') + { + if (argIndex >= lv.size()) + { + throw new InternalExpressionException("Not enough arguments for " + m.group(0)); + } + args.add(lv.get(argIndex).readInteger()); + argIndex++; + } + else if (fmt == 'a' || fmt == 'e' || fmt == 'f' || fmt == 'g') + { + if (argIndex >= lv.size()) + { + throw new InternalExpressionException("Not enough arguments for " + m.group(0)); + } + args.add(lv.get(argIndex).readDoubleNumber()); + argIndex++; + } + else if (fmt == 'b') + { + if (argIndex >= lv.size()) + { + throw new InternalExpressionException("Not enough arguments for " + m.group(0)); + } + args.add(lv.get(argIndex).getBoolean()); + argIndex++; + } + else if (fmt != '%') + { + throw new InternalExpressionException("Format not supported: " + m.group(6)); + } + + i = m.end(); + } + else + { + // No more valid format specifiers. Check for possible invalid + // format specifiers. + // [[scarpet]] but we skip it and let the String.format fail + break; + } + } + try + { + return new StringValue(String.format(Locale.ROOT, format, args.toArray())); + } + catch (IllegalFormatException ife) + { + throw new InternalExpressionException("Illegal string format: " + ife.getMessage()); + } + }); + + expression.addUnaryFunction("lower", v -> new StringValue(v.getString().toLowerCase(Locale.ROOT))); + + expression.addUnaryFunction("upper", v -> new StringValue(v.getString().toUpperCase(Locale.ROOT))); + + expression.addUnaryFunction("title", v -> new StringValue(titleCase(v.getString()))); + + expression.addFunction("replace", lv -> + { + if (lv.size() != 3 && lv.size() != 2) + { + throw new InternalExpressionException("'replace' expects string to read, pattern regex, and optional replacement string"); + } + String data = lv.get(0).getString(); + String regex = lv.get(1).getString(); + String replacement = ""; + if (lv.size() == 3) + { + replacement = lv.get(2).getString(); + } + try + { + return new StringValue(data.replaceAll(regex, replacement)); + } + catch (PatternSyntaxException pse) + { + throw new InternalExpressionException("Incorrect pattern for 'replace': " + pse.getMessage()); + } + }); + + expression.addFunction("replace_first", lv -> + { + if (lv.size() != 3 && lv.size() != 2) + { + throw new InternalExpressionException("'replace_first' expects string to read, pattern regex, and optional replacement string"); + } + String data = lv.get(0).getString(); + String regex = lv.get(1).getString(); + String replacement = ""; + if (lv.size() == 3) + { + replacement = lv.get(2).getString(); + } + return new StringValue(data.replaceFirst(regex, replacement)); + }); + + expression.addUnaryFunction("type", v -> new StringValue(v.getTypeString())); + expression.addUnaryFunction("length", v -> new NumericValue(v.length())); + expression.addContextFunction("rand", -1, (c, t, lv) -> + { + int argsize = lv.size(); + Random randomizer = Sys.randomizer; + if (argsize != 1 && argsize != 2) + { + throw new InternalExpressionException("'rand' takes one (range) or two arguments (range and seed)"); + } + if (argsize == 2) + { + randomizer = c.host.getRandom(NumericValue.asNumber(lv.get(1)).getLong()); + } + Value argument = lv.get(0); + if (argument instanceof final ListValue listValue) + { + List list = listValue.getItems(); + return list.get(randomizer.nextInt(list.size())); + } + double value = NumericValue.asNumber(argument).getDouble() * randomizer.nextDouble(); + return t == Context.BOOLEAN ? BooleanValue.of(value >= 1.0D) : new NumericValue(value); + }); + expression.addContextFunction("reset_seed", 1, (c, t, lv) -> { + boolean gotIt = c.host.resetRandom(NumericValue.asNumber(lv.get(0)).getLong()); + return BooleanValue.of(gotIt); + }); + + expression.addFunction("perlin", lv -> + { + PerlinNoiseSampler sampler; + Value x; + Value y; + Value z; + + if (lv.size() >= 4) + { + x = lv.get(0); + y = lv.get(1); + z = lv.get(2); + sampler = PerlinNoiseSampler.getPerlin(NumericValue.asNumber(lv.get(3)).getLong()); + } + else + { + sampler = PerlinNoiseSampler.instance; + y = Value.NULL; + z = Value.NULL; + if (lv.isEmpty()) + { + throw new InternalExpressionException("'perlin' requires at least one dimension to sample from"); + } + x = NumericValue.asNumber(lv.get(0)); + if (lv.size() > 1) + { + y = NumericValue.asNumber(lv.get(1)); + if (lv.size() > 2) + { + z = NumericValue.asNumber(lv.get(2)); + } + } + } + + double result; + + if (z.isNull()) + { + result = y.isNull() + ? sampler.sample1d(NumericValue.asNumber(x).getDouble()) + : sampler.sample2d(NumericValue.asNumber(x).getDouble(), NumericValue.asNumber(y).getDouble()); + } + else + { + result = sampler.sample3d( + NumericValue.asNumber(x).getDouble(), + NumericValue.asNumber(y).getDouble(), + NumericValue.asNumber(z).getDouble()); + } + return new NumericValue(result); + }); + + expression.addFunction("simplex", lv -> + { + SimplexNoiseSampler sampler; + Value x; + Value y; + Value z; + + if (lv.size() >= 4) + { + x = lv.get(0); + y = lv.get(1); + z = lv.get(2); + sampler = SimplexNoiseSampler.getSimplex(NumericValue.asNumber(lv.get(3)).getLong()); + } + else + { + sampler = SimplexNoiseSampler.instance; + z = Value.NULL; + if (lv.size() < 2) + { + throw new InternalExpressionException("'simplex' requires at least two dimensions to sample from"); + } + x = NumericValue.asNumber(lv.get(0)); + y = NumericValue.asNumber(lv.get(1)); + if (lv.size() > 2) + { + z = NumericValue.asNumber(lv.get(2)); + } + } + double result; + + if (z.isNull()) + { + result = sampler.sample2d(NumericValue.asNumber(x).getDouble(), NumericValue.asNumber(y).getDouble()); + } + else + { + result = sampler.sample3d( + NumericValue.asNumber(x).getDouble(), + NumericValue.asNumber(y).getDouble(), + NumericValue.asNumber(z).getDouble()); + } + return new NumericValue(result); + }); + + expression.addUnaryFunction("print", v -> + { + System.out.println(v.getString()); + return v; // pass through for variables + }); + + expression.addContextFunction("time", 0, (c, t, lv) -> + new NumericValue((System.nanoTime() / 1000.0) / 1000.0)); + + expression.addContextFunction("unix_time", 0, (c, t, lv) -> + new NumericValue(System.currentTimeMillis())); + + expression.addFunction("convert_date", lv -> + { + int argsize = lv.size(); + if (lv.isEmpty()) + { + throw new InternalExpressionException("'convert_date' requires at least one parameter"); + } + Value value = lv.get(0); + if (argsize == 1 && !(value instanceof ListValue)) + { + Calendar cal = new GregorianCalendar(Locale.ROOT); + cal.setTimeInMillis(NumericValue.asNumber(value, "timestamp").getLong()); + int weekday = cal.get(Calendar.DAY_OF_WEEK) - 1; + if (weekday == 0) + { + weekday = 7; + } + return ListValue.ofNums( + cal.get(Calendar.YEAR), + cal.get(Calendar.MONTH) + 1, + cal.get(Calendar.DAY_OF_MONTH), + cal.get(Calendar.HOUR_OF_DAY), + cal.get(Calendar.MINUTE), + cal.get(Calendar.SECOND), + weekday, + cal.get(Calendar.DAY_OF_YEAR), + cal.get(Calendar.WEEK_OF_YEAR) + ); + } + else if (value instanceof final ListValue list) + { + lv = list.getItems(); + argsize = lv.size(); + } + Calendar cal = new GregorianCalendar(0, Calendar.JANUARY, 1, 0, 0, 0); + + if (argsize == 3) + { + cal.set( + NumericValue.asNumber(lv.get(0)).getInt(), + NumericValue.asNumber(lv.get(1)).getInt() - 1, + NumericValue.asNumber(lv.get(2)).getInt() + ); + } + else if (argsize == 6) + { + cal.set( + NumericValue.asNumber(lv.get(0)).getInt(), + NumericValue.asNumber(lv.get(1)).getInt() - 1, + NumericValue.asNumber(lv.get(2)).getInt(), + NumericValue.asNumber(lv.get(3)).getInt(), + NumericValue.asNumber(lv.get(4)).getInt(), + NumericValue.asNumber(lv.get(5)).getInt() + ); + } + else + { + throw new InternalExpressionException("Date conversion requires 3 arguments for Dates or 6 arguments, for time"); + } + return new NumericValue(cal.getTimeInMillis()); + }); + + // lazy cause evaluates expression multiple times + expression.addLazyFunction("profile_expr", 1, (c, t, lv) -> + { + LazyValue lazy = lv.get(0); + long end = System.nanoTime() + 50000000L; + long it = 0; + while (System.nanoTime() < end) + { + lazy.evalValue(c); + it++; + } + Value res = new NumericValue(it); + return (cc, tt) -> res; + }); + + expression.addContextFunction("var", 1, (c, t, lv) -> + expression.getOrSetAnyVariable(c, lv.get(0).getString()).evalValue(c)); + + expression.addContextFunction("undef", 1, (c, t, lv) -> + { + Value remove = lv.get(0); + if (remove instanceof FunctionValue) + { + c.host.delFunction(expression.module, remove.getString()); + return Value.NULL; + } + String varname = remove.getString(); + boolean isPrefix = varname.endsWith("*"); + if (isPrefix) + { + varname = varname.replaceAll("\\*+$", ""); + } + if (isPrefix) + { + c.host.delFunctionWithPrefix(expression.module, varname); + if (varname.startsWith("global_")) + { + c.host.delGlobalVariableWithPrefix(expression.module, varname); + } + else if (!varname.startsWith("_")) + { + c.removeVariablesMatching(varname); + } + } + else + { + c.host.delFunction(expression.module, varname); + if (varname.startsWith("global_")) + { + c.host.delGlobalVariable(expression.module, varname); + } + else if (!varname.startsWith("_")) + { + c.delVariable(varname); + } + } + return Value.NULL; + }); + + //deprecate + expression.addContextFunction("vars", 1, (c, t, lv) -> + { + String prefix = lv.get(0).getString(); + List values = new ArrayList<>(); + if (prefix.startsWith("global")) + { + c.host.globalVariableNames(expression.module, s -> s.startsWith(prefix)).forEach(s -> values.add(new StringValue(s))); + } + else + { + c.getAllVariableNames().stream().filter(s -> s.startsWith(prefix)).forEach(s -> values.add(new StringValue(s))); + } + return ListValue.wrap(values); + }); + + // lazy cause default expression may not be executed if not needed + expression.addLazyFunction("system_variable_get", (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'system_variable_get' expects at least a key to be fetched"); + } + Value key = lv.get(0).evalValue(c); + if (lv.size() > 1) + { + c.host.scriptServer().systemGlobals.computeIfAbsent(key, k -> lv.get(1).evalValue(c)); + } + Value res = c.host.scriptServer().systemGlobals.get(key); + return res == null ? LazyValue.NULL : ((cc, tt) -> res); + }); + + expression.addContextFunction("system_variable_set", 2, (c, t, lv) -> + { + Value res = c.host.scriptServer().systemGlobals.put(lv.get(0), lv.get(1)); + return res == null ? Value.NULL : res; + }); + } + + public static String titleCase(String str) { + if (str.isEmpty()) { + return str; + } + str = str.toLowerCase(); + char[] buffer = str.toCharArray(); + boolean capitalizeNext = true; + for (int i = 0; i < buffer.length; i++) { + char ch = buffer[i]; + if (Character.isWhitespace(ch)) { + capitalizeNext = true; + } else if (capitalizeNext) { + buffer[i] = Character.toTitleCase(ch); + capitalizeNext = false; + } + } + return new String(buffer); + } + +} diff --git a/src/main/java/carpet/script/language/Threading.java b/src/main/java/carpet/script/language/Threading.java new file mode 100644 index 0000000..a129d09 --- /dev/null +++ b/src/main/java/carpet/script/language/Threading.java @@ -0,0 +1,191 @@ +package carpet.script.language; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.argument.FunctionArgument; +import carpet.script.exception.ExitStatement; +import carpet.script.exception.InternalExpressionException; +import carpet.script.value.BooleanValue; +import carpet.script.value.NumericValue; +import carpet.script.value.ThreadValue; +import carpet.script.value.Value; + +public class Threading +{ + public static void apply(Expression expression) + { + expression.addFunctionWithDelegation("task", -1, false, false, (c, t, expr, tok, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'task' requires at least function to call as a parameter"); + } + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 0, false, true); + ThreadValue thread = new ThreadValue(Value.NULL, functionArgument.function, expr, tok, c, functionArgument.checkedArgs()); + Thread.yield(); + return thread; + }); + + expression.addFunctionWithDelegation("task_thread", -1, false, false, (c, t, expr, tok, lv) -> + { + if (lv.size() < 2) + { + throw new InternalExpressionException("'task' requires at least function to call as a parameter"); + } + Value queue = lv.get(0); + FunctionArgument functionArgument = FunctionArgument.findIn(c, expression.module, lv, 1, false, true); + ThreadValue thread = new ThreadValue(queue, functionArgument.function, expr, tok, c, functionArgument.checkedArgs()); + Thread.yield(); + return thread; + }); + + + expression.addContextFunction("task_count", -1, (c, t, lv) -> + (!lv.isEmpty()) ? new NumericValue(c.host.taskCount(lv.get(0))) : new NumericValue(c.host.taskCount())); + + expression.addUnaryFunction("task_value", v -> + { + if (!(v instanceof final ThreadValue tv)) + { + throw new InternalExpressionException("'task_value' could only be used with a task value"); + } + return tv.getValue(); + }); + + expression.addUnaryFunction("task_join", v -> + { + if (!(v instanceof final ThreadValue tv)) + { + throw new InternalExpressionException("'task_join' could only be used with a task value"); + } + return tv.join(); + }); + + expression.addLazyFunction("task_dock", 1, (c, t, lv) -> + // pass through placeholder + // implmenetation should dock the task on the main thread. + lv.get(0) + ); + + expression.addUnaryFunction("task_completed", v -> + { + if (!(v instanceof final ThreadValue tv)) + { + throw new InternalExpressionException("'task_completed' could only be used with a task value"); + } + return BooleanValue.of(tv.isFinished()); + }); + + // lazy cause expr is evaluated in the same type + expression.addLazyFunction("synchronize", (c, t, lv) -> + { + if (lv.isEmpty()) + { + throw new InternalExpressionException("'synchronize' require at least an expression to synchronize"); + } + Value lockValue = Value.NULL; + int ind = 0; + if (lv.size() == 2) + { + lockValue = lv.get(0).evalValue(c); + ind = 1; + } + synchronized (c.host.getLock(lockValue)) + { + Value ret = lv.get(ind).evalValue(c, t); + return (ct, tt) -> ret; + } + }); + + // lazy since exception expression is very conditional + expression.addLazyFunction("sleep", (c, t, lv) -> + { + long time = lv.isEmpty() ? 0L : NumericValue.asNumber(lv.get(0).evalValue(c)).getLong(); + boolean interrupted = false; + try + { + if (Thread.interrupted()) + { + interrupted = true; + } + if (time > 0) + { + Thread.sleep(time); + } + Thread.yield(); + } + catch (InterruptedException ignored) + { + interrupted = true; + } + if (interrupted) + { + Value exceptionally = Value.NULL; + if (lv.size() > 1) + { + exceptionally = lv.get(1).evalValue(c); + } + throw new ExitStatement(exceptionally); + } + return (cc, tt) -> new NumericValue(time); // pass through for variables + }); + + expression.addLazyFunction("yield", (c, t, lv) -> + { + if (c.getThreadContext() == null) + { + throw new InternalExpressionException("'yield' can only be used in a task"); + } + if (lv.isEmpty()) + { + throw new InternalExpressionException("'yield' requires at least one argument"); + } + boolean lock = lv.size() > 1 && lv.get(1).evalValue(c, Context.BOOLEAN).getBoolean(); + Value value = lv.get(0).evalValue(c); + Value ret = c.getThreadContext().ping(value, lock); + return (cc, tt) -> ret; + }); + + expression.addLazyFunction("task_send", 2, (c, t, lv) -> + { + Value threadValue = lv.get(0).evalValue(c); + if (!(threadValue instanceof ThreadValue thread)) + { + throw new InternalExpressionException("'task_next' requires a task value"); + } + if (!thread.isCoroutine) + { + throw new InternalExpressionException("'task_next' requires a coroutine task value"); + } + Value ret = lv.get(1).evalValue(c); + thread.send(ret); + return (cc, tt) -> Value.NULL; + }); + + expression.addLazyFunction("task_await", 1, (c, t, lv) -> + { + Value threadValue = lv.get(0).evalValue(c); + if (!(threadValue instanceof ThreadValue thread)) + { + throw new InternalExpressionException("'task_await' requires a task value"); + } + if (!thread.isCoroutine) + { + throw new InternalExpressionException("'task_await' requires a coroutine task value"); + } + Value ret = thread.next(); + return ret == Value.EOL ? ((cc, tt) -> Value.NULL) : ((cc, tt) -> ret); + }); + + expression.addLazyFunction("task_ready", 1, (c, t, lv) -> + { + Value threadValue = lv.get(0).evalValue(c); + if (!(threadValue instanceof ThreadValue thread)) + { + throw new InternalExpressionException("'task_ready' requires a task value"); + } + boolean ret = thread.isCoroutine && thread.hasNext(); + return (cc, tt) -> BooleanValue.of(ret); + }); + } +} diff --git a/src/main/java/carpet/script/language/package-info.java b/src/main/java/carpet/script/language/package-info.java new file mode 100644 index 0000000..812e4dc --- /dev/null +++ b/src/main/java/carpet/script/language/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.language; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/package-info.java b/src/main/java/carpet/script/package-info.java new file mode 100644 index 0000000..e2c4fe3 --- /dev/null +++ b/src/main/java/carpet/script/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/utils/AppStoreManager.java b/src/main/java/carpet/script/utils/AppStoreManager.java new file mode 100644 index 0000000..963cd54 --- /dev/null +++ b/src/main/java/carpet/script/utils/AppStoreManager.java @@ -0,0 +1,489 @@ +package carpet.script.utils; + +import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; +import carpet.script.exception.InternalExpressionException; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.value.MapValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.storage.LevelResource; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.charset.StandardCharsets; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.io.IOUtils; + +import javax.annotation.Nullable; + +/** + * A class used to save scarpet app store scripts to disk + */ +public class AppStoreManager +{ + /** + * A local copy of the scarpet repo's file structure, to avoid multiple queries to github.com while typing out the + * {@code /script download} command and getting the suggestions. + */ + private static StoreNode APP_STORE_ROOT = StoreNode.folder(null, ""); + private static long storeErrorTime = 0; + + /** + * This is the base link to the scarpet app repo from the github api. + */ + private static String scarpetRepoLink = "https://api.github.com/repos/gnembon/scarpet/contents/programs/"; + + public static void setScarpetRepoLink(@Nullable String link) + { + APP_STORE_ROOT = AppStoreManager.StoreNode.folder(null, ""); + scarpetRepoLink = link; + } + + public static boolean enabled() + { + return scarpetRepoLink != null; + } + + private record AppInfo(String name, String url, StoreNode source) + { + } + + public static class StoreNode + { + public String name; + @Nullable + public StoreNode parent; + public Map children; + public boolean sealed; + public String value; + + public static StoreNode folder(@Nullable StoreNode parent, String name) + { + StoreNode node = new StoreNode(parent, name); + node.children = new HashMap<>(); + node.value = null; + node.sealed = false; + return node; + } + + public static StoreNode scriptFile(StoreNode parent, String name, String value) + { + StoreNode node = new StoreNode(parent, name); + node.children = null; + node.value = value; + node.sealed = true; + return node; + } + + public boolean isLeaf() + { + return value != null; + } + + public String pathElement() + { + return name + (isLeaf() ? "" : "/"); + } + + public String getPath() + { + return createPrePath().toString(); + } + + private StringBuilder createPrePath() + { + return this == APP_STORE_ROOT ? new StringBuilder() : parent.createPrePath().append(pathElement()); + } + + private StoreNode(@Nullable StoreNode parent, String name) + { + this.parent = parent; + this.name = name; + this.sealed = false; + } + + public synchronized void fillChildren(@Nullable CommandSourceStack source) throws IOException + { + if (sealed) + { + return; + } + if (!enabled()) + { + throw new IOException("Accessing scarpet app repo is disabled"); + } + if (System.currentTimeMillis() - storeErrorTime < 30000) + { + if (source != null) + { + Carpet.Messenger_message(source, "di App store is not available yet"); + } + return; + } + + String queryPath = scarpetRepoLink + getPath(); + String response; + try + { + response = IOUtils.toString(new URL(queryPath), StandardCharsets.UTF_8); + } + catch (IOException e) + { + if (source != null) + { + Carpet.Messenger_message(source, "r Scarpet app store is not available at the moment, try in a minute"); + } + storeErrorTime = System.currentTimeMillis(); + // Not sealing to allow retrying + throw new IOException("Problems fetching " + queryPath, e); + } + JsonArray files = JsonParser.parseString(response).getAsJsonArray(); + for (JsonElement je : files) + { + JsonObject jo = je.getAsJsonObject(); + String elementName = jo.get("name").getAsString(); + if (jo.get("type").getAsString().equals("dir")) + { + children.put(elementName, folder(this, elementName)); + } + else// if (name.matches("(\\w+\\.scl?)")) + { + String url = jo.get("download_url").getAsString(); + children.put(elementName, scriptFile(this, elementName, url)); + } + } + sealed = true; + } + + /** + * Returns true if doing down the directory structure cannot continue since the matching element is either a leaf or + * a string not matching of any node. + */ + public boolean cannotContinueFor(String pathElement, CommandSourceStack source) throws IOException + { + if (isLeaf()) + { + return true; + } + fillChildren(source); + return !children.containsKey(pathElement); + } + + public List createPathSuggestions(CommandSourceStack source) throws IOException + { + if (isLeaf()) + { + return name.endsWith(".sc") ? Collections.singletonList(getPath()) : Collections.emptyList(); + } + fillChildren(source); + String prefix = getPath(); + return children.values().stream(). + filter(n -> (!n.isLeaf() || n.name.endsWith(".sc"))). + map(s -> prefix + s.pathElement().replaceAll("/$", "")).toList(); + } + + public StoreNode drillDown(String pathElement, CommandSourceStack source) throws IOException + { + if (isLeaf()) + { + throw new IOException(pathElement + " is not a folder"); + } + fillChildren(source); + if (!children.containsKey(pathElement)) + { + throw new IOException("Folder " + pathElement + " is not present"); + } + return children.get(pathElement); + } + + public String getValue(String file, CommandSourceStack source) throws IOException + { + StoreNode leaf = drillDown(file, source); + if (!leaf.isLeaf()) + { + throw new IOException(file + " is not a file"); + } + return leaf.value; + } + } + + /** + * This method searches for valid file names from the user-inputted string, e.g if the user has thus far typed + * {@code survival/a} then it will return all the files in the {@code survival} directory of the scarpet repo (and + * will automatically highlight those starting with a), and the string {@code survival/} as the current most valid path. + * + * @param currentPath The path down which we want to search for files + * @return A pair of the current valid path, as well as the set of all the file/directory names at the end of that path + */ + public static List suggestionsFromPath(String currentPath, CommandSourceStack source) throws IOException + { + String[] path = currentPath.split("/"); + StoreNode appKiosk = APP_STORE_ROOT; + for (String pathElement : path) + { + if (appKiosk.cannotContinueFor(pathElement, source)) + { + break; + } + appKiosk = appKiosk.children.get(pathElement); + } + List filteredSuggestions = appKiosk.createPathSuggestions(source).stream().filter(s -> s.startsWith(currentPath)).toList(); + if (filteredSuggestions.size() == 1 && !appKiosk.isLeaf()) + { + return suggestionsFromPath(filteredSuggestions.get(0), source); // Start suggesting directory contents + } + return filteredSuggestions; + } + + + /** + * Downloads script and saves it to appropriate place. + * + * @param path The user-inputted path to the script + * @return {@code 1} if we succesfully saved the script, {@code 0} otherwise + */ + + public static int downloadScript(CommandSourceStack source, String path) + { + AppInfo nodeInfo = getFileNode(path, source); + return downloadScript(source, path, nodeInfo, false); + } + + private static int downloadScript(CommandSourceStack source, String path, AppInfo nodeInfo, boolean useTrash) + { + String code; + try + { + code = IOUtils.toString(new URL(nodeInfo.url()), StandardCharsets.UTF_8); + } + catch (IOException e) + { + throw new CommandRuntimeException(Carpet.Messenger_compose("rb Failed to obtain app file content: " + e.getMessage())); + } + if (!saveScriptToFile(source, path, nodeInfo.name(), code, useTrash)) + { + return 0; + } + boolean success = Vanilla.MinecraftServer_getScriptServer(source.getServer()).addScriptHost(source, nodeInfo.name().replaceFirst("\\.sc$", ""), null, true, false, false, nodeInfo.source()); + return success ? 1 : 0; + } + + /** + * Gets the code once the user inputs the command. + * + * @param appPath The user inputted path to the scarpet script + * @return Pair of app file name and content + */ + public static AppInfo getFileNode(String appPath, CommandSourceStack source) + { + return getFileNodeFrom(APP_STORE_ROOT, appPath, source); + } + + public static AppInfo getFileNodeFrom(StoreNode start, String appPath, CommandSourceStack source) + { + String[] path = appPath.split("/"); + StoreNode appKiosk = start; + try + { + for (String pathElement : Arrays.copyOfRange(path, 0, path.length - 1)) + { + appKiosk = appKiosk.drillDown(pathElement, source); + } + String appName = path[path.length - 1]; + appKiosk.getValue(appName, source); + return new AppInfo(appName, appKiosk.getValue(appName, source), appKiosk); + } + catch (IOException e) + { + throw new CommandRuntimeException(Carpet.Messenger_compose("rb '" + appPath + "' is not a valid path to a scarpet app: " + e.getMessage())); + } + } + + + public static boolean saveScriptToFile(CommandSourceStack source, String path, String appFileName, String code, boolean useTrash) + { + Path scriptLocation = source.getServer().getWorldPath(LevelResource.ROOT).resolve("scripts").toAbsolutePath().resolve(appFileName); + try + { + Files.createDirectories(scriptLocation.getParent()); + if (Files.exists(scriptLocation)) + { + if (useTrash) + { + Files.createDirectories(scriptLocation.getParent().resolve("trash")); + Path trashPath = scriptLocation.getParent().resolve("trash").resolve(path); + int i = 0; + while (Files.exists(trashPath)) + { + String[] nameAndExtension = appFileName.split("\\."); + String newFileName = String.format(nameAndExtension[0] + "%02d." + nameAndExtension[1], i); + trashPath = trashPath.getParent().resolve(newFileName); + i++; + } + Files.move(scriptLocation, trashPath); + } + Carpet.Messenger_message(source, String.format("gi Note: replaced existing app '%s'" + (useTrash ? " (old moved to /trash folder)" : ""), appFileName)); + } + BufferedWriter writer = Files.newBufferedWriter(scriptLocation); + writer.write(code); + writer.close(); + } + catch (IOException e) + { + Carpet.Messenger_message(source, "r Error while downloading app: " + e); + CarpetScriptServer.LOG.warn("Error while downloading app", e); + return false; + } + return true; + } + + public static void writeUrlToFile(String url, Path destination) throws IOException + { + try (InputStream in = new URL(url).openStream()) + { + Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING); + } + } + + private static String getFullContentUrl(String original, StoreNode storeSource, CommandSourceStack source) + { + if (original.matches("^https?://.*$")) // We've got a full url here: Just use it + { + return original; + } + if (original.charAt(0) == '/') // We've got an absolute path: Use app store root + { + return getFileNode(original.substring(1), source).url(); + } + return getFileNodeFrom(storeSource, original, source).url(); // Relative path: Use download location + } + + public static void addResource(CarpetScriptHost carpetScriptHost, StoreNode storeSource, Value resource) + { + if (!(resource instanceof final MapValue map)) + { + throw new InternalExpressionException("This is not a valid resource map: " + resource.getString()); + } + Map resourceMap = map.getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), Map.Entry::getValue)); + if (!resourceMap.containsKey("source")) + { + throw new InternalExpressionException("Missing 'source' field in resource descriptor: " + resource.getString()); + } + String source = resourceMap.get("source").getString(); + String contentUrl = getFullContentUrl(source, storeSource, carpetScriptHost.responsibleSource); + String target = resourceMap.computeIfAbsent("target", k -> new StringValue(contentUrl.substring(contentUrl.lastIndexOf('/') + 1))).getString(); + boolean shared = resourceMap.getOrDefault("shared", Value.FALSE).getBoolean(); + + if (!carpetScriptHost.applyActionForResource(target, shared, p -> { + try + { + writeUrlToFile(contentUrl, p); + } + catch (IOException e) + { + throw new InternalExpressionException("Unable to write resource " + target + ": " + e); + } + })) + { + throw new InternalExpressionException("Unable to write resource " + target); + } + CarpetScriptServer.LOG.info("Downloaded resource " + target + " from " + contentUrl); + } + + /** + * Gets a new StoreNode for an app's dependency with proper relativeness. Will be null if it comes from an external URL + * + * @param originalSource The StoreNode from the container's app + * @param sourceString The string the app specified as source + * @param contentUrl The full content URL, from {@link #getFullContentUrl(String, StoreNode, CommandSourceStack)} + * @return A {@link StoreNode} that can be used in an app that came from the provided source + */ + @Nullable + private static StoreNode getNewStoreNode(CommandSourceStack commandSource, StoreNode originalSource, String sourceString, String contentUrl) + { + StoreNode next = originalSource; + if (sourceString == contentUrl) // External URL (check getFullUrlContent) + { + return null; + } + if (sourceString.charAt(0) == '/') // Absolute URL + { + next = APP_STORE_ROOT; + sourceString = sourceString.substring(1); + } + String[] dirs = sourceString.split("/"); + try + { + for (int i = 0; i < dirs.length - 1; i++) + { + next = next.drillDown(dirs[i], commandSource); + } + } + catch (IOException e) + { + return null; // Should never happen, but let's not give a potentially incorrect node just in case + } + return next; + } + + public static void addLibrary(CarpetScriptHost carpetScriptHost, StoreNode storeSource, Value library) + { + if (!(library instanceof final MapValue map)) + { + throw new InternalExpressionException("This is not a valid library map: " + library.getString()); + } + Map libraryMap = map.getMap().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().getString(), e -> e.getValue().getString())); + String source = libraryMap.get("source"); + String contentUrl = getFullContentUrl(source, storeSource, carpetScriptHost.responsibleSource); + String target = libraryMap.computeIfAbsent("target", k -> contentUrl.substring(contentUrl.lastIndexOf('/') + 1)); + if (!(contentUrl.endsWith(".sc") || contentUrl.endsWith(".scl"))) + { + throw new InternalExpressionException("App resource type must download a scarpet app or library."); + } + if (target.indexOf('/') != -1) + { + throw new InternalExpressionException("App resource tried to leave script reserved space"); + } + try + { + downloadScript(carpetScriptHost.responsibleSource, target, new AppInfo(target, contentUrl, getNewStoreNode(carpetScriptHost.responsibleSource, storeSource, source, contentUrl)), true); + } + catch (CommandRuntimeException e) + { + throw new InternalExpressionException("Error when installing app dependencies: " + e); + } + CarpetScriptServer.LOG.info("Downloaded app " + target + " from " + contentUrl); + } + + public static class CommandRuntimeException extends RuntimeException { + private final Component message; + + public CommandRuntimeException(Component message) { + super(message.getString(), null, false, false); + this.message = message; + } + + public Component getComponent() { + return message; + } + } +} diff --git a/src/main/java/carpet/script/utils/BiomeInfo.java b/src/main/java/carpet/script/utils/BiomeInfo.java new file mode 100644 index 0000000..4cd0776 --- /dev/null +++ b/src/main/java/carpet/script/utils/BiomeInfo.java @@ -0,0 +1,46 @@ +package carpet.script.utils; + +import carpet.script.external.Vanilla; +import carpet.script.value.ListValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.BiFunction; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; + +public class BiomeInfo +{ + public static final Map> biomeFeatures = new HashMap<>() + {{ + put("tags", (w, b) -> ListValue.wrap(w.registryAccess().registryOrThrow(Registries.BIOME).getTags().filter(p -> p.getSecond().stream().anyMatch(h -> h.value() == b)).map(p -> p.getFirst().location()).map(ValueConversions::of))); + put("temperature", (w, b) -> NumericValue.of(b.getBaseTemperature())); + put("fog_color", (w, b) -> ValueConversions.ofRGB(b.getSpecialEffects().getFogColor())); + put("foliage_color", (w, b) -> ValueConversions.ofRGB(b.getSpecialEffects().getFoliageColorOverride().orElse(4764952))); // client Biome.getDefaultFoliageColor + put("sky_color", (w, b) -> ValueConversions.ofRGB(b.getSpecialEffects().getSkyColor())); + put("water_color", (w, b) -> ValueConversions.ofRGB(b.getSpecialEffects().getWaterColor())); + put("water_fog_color", (w, b) -> ValueConversions.ofRGB(b.getSpecialEffects().getWaterFogColor())); + put("humidity", (w, b) -> NumericValue.of(Vanilla.Biome_getClimateSettings(b).downfall())); + put("precipitation", (w, b) -> StringValue.of(b.getPrecipitationAt(new BlockPos(0, w.getSeaLevel(), 0)).name().toLowerCase(Locale.ROOT))); + put("features", (w, b) -> { + Registry> registry = w.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE); + return ListValue.wrap( + b.getGenerationSettings().features().stream().map(step -> + ListValue.wrap(step.stream().map(cfp -> + ValueConversions.of(registry.getKey(cfp.value().feature().value()))) + ) + ) + ); + }); + }}; +} diff --git a/src/main/java/carpet/script/utils/Colors.java b/src/main/java/carpet/script/utils/Colors.java new file mode 100644 index 0000000..8b5a10c --- /dev/null +++ b/src/main/java/carpet/script/utils/Colors.java @@ -0,0 +1,156 @@ +package carpet.script.utils; + +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.material.MapColor; + +import java.util.Map; + +import static java.util.Map.entry; + +public class Colors +{ + public static final Map soundName = Map.ofEntries( + entry(SoundType.WOOD, "wood" ), + entry(SoundType.GRAVEL, "gravel"), + entry(SoundType.GRASS, "grass" ), + entry(SoundType.LILY_PAD, "lily_pad"), + entry(SoundType.STONE, "stone" ), + entry(SoundType.METAL, "metal" ), + entry(SoundType.GLASS , "glass" ), + entry(SoundType.WOOL , "wool" ), + entry(SoundType.SAND , "sand" ), + entry(SoundType.SNOW , "snow" ), + entry(SoundType.POWDER_SNOW , "powder_snow" ), + entry(SoundType.LADDER, "ladder"), + entry(SoundType.ANVIL , "anvil" ), + entry(SoundType.SLIME_BLOCK , "slime" ), + entry(SoundType.HONEY_BLOCK , "honey" ), + entry(SoundType.WET_GRASS , "sea_grass" ), + entry(SoundType.CORAL_BLOCK , "coral" ), + entry(SoundType.BAMBOO , "bamboo" ), + entry(SoundType.BAMBOO_SAPLING , "shoots" ), + entry(SoundType.SCAFFOLDING , "scaffolding" ), + entry(SoundType.SWEET_BERRY_BUSH , "berry" ), + entry(SoundType.CROP , "crop" ), + entry(SoundType.HARD_CROP , "stem" ), + entry(SoundType.VINE , "vine" ), + entry(SoundType.NETHER_WART , "wart" ), + entry(SoundType.LANTERN , "lantern" ), + entry(SoundType.STEM, "fungi_stem"), + entry(SoundType.NYLIUM, "nylium"), + entry(SoundType.FUNGUS, "fungus"), + entry(SoundType.ROOTS, "roots"), + entry(SoundType.SHROOMLIGHT, "shroomlight"), + entry(SoundType.WEEPING_VINES, "weeping_vine"), + entry(SoundType.TWISTING_VINES, "twisting_vine"), + entry(SoundType.SOUL_SAND, "soul_sand"), + entry(SoundType.SOUL_SOIL, "soul_soil"), + entry(SoundType.BASALT, "basalt"), + entry(SoundType.WART_BLOCK, "wart"), + entry(SoundType.NETHERRACK, "netherrack"), + entry(SoundType.NETHER_BRICKS, "nether_bricks"), + entry(SoundType.NETHER_SPROUTS, "nether_sprouts"), + entry(SoundType.NETHER_ORE, "nether_ore"), + entry(SoundType.BONE_BLOCK, "bone"), + entry(SoundType.NETHERITE_BLOCK, "netherite"), + entry(SoundType.ANCIENT_DEBRIS, "ancient_debris"), + entry(SoundType.LODESTONE, "lodestone"), + entry(SoundType.CHAIN, "chain"), + entry(SoundType.NETHER_GOLD_ORE, "nether_gold_ore"), + entry(SoundType.GILDED_BLACKSTONE, "gilded_blackstone"), + entry(SoundType.CANDLE, "candle"), + entry(SoundType.AMETHYST, "amethyst"), + entry(SoundType.AMETHYST_CLUSTER, "amethyst_cluster"), + entry(SoundType.SMALL_AMETHYST_BUD, "small_amethyst_bud"), + entry(SoundType.MEDIUM_AMETHYST_BUD, "medium_amethyst_bud"), + entry(SoundType.LARGE_AMETHYST_BUD, "large_amethyst_bud"), + + entry(SoundType.TUFF, "tuff"), + entry(SoundType.CALCITE, "calcite"), + entry(SoundType.DRIPSTONE_BLOCK, "dripstone"), + entry(SoundType.POINTED_DRIPSTONE, "pointed_dripstone"), + entry(SoundType.COPPER, "copper"), + entry(SoundType.CAVE_VINES, "cave_vine"), + entry(SoundType.SPORE_BLOSSOM, "spore_blossom"), + entry(SoundType.AZALEA, "azalea"), + entry(SoundType.FLOWERING_AZALEA, "flowering_azalea"), + entry(SoundType.MOSS_CARPET, "moss_carpet"), + entry(SoundType.MOSS, "moss"), + entry(SoundType.BIG_DRIPLEAF, "big_dripleaf"), + entry(SoundType.SMALL_DRIPLEAF, "small_dripleaf"), + entry(SoundType.ROOTED_DIRT, "rooted_dirt"), + entry(SoundType.HANGING_ROOTS, "hanging_roots"), + entry(SoundType.AZALEA_LEAVES, "azalea_leaves"), + entry(SoundType.SCULK_SENSOR, "sculk_sensor"), + entry(SoundType.GLOW_LICHEN, "glow_lichen"), + entry(SoundType.DEEPSLATE, "deepslate"), + entry(SoundType.DEEPSLATE_BRICKS, "deepslate_bricks"), + entry(SoundType.DEEPSLATE_TILES, "deepslate_tiles"), + entry(SoundType.POLISHED_DEEPSLATE, "polished_deepslate") + ); + + public static final Map mapColourName = Map.ofEntries( + entry(MapColor.NONE , "air" ), + entry(MapColor.GRASS , "grass" ), + entry(MapColor.SAND , "sand" ), + entry(MapColor.WOOL , "wool" ), + entry(MapColor.FIRE , "tnt" ), + entry(MapColor.ICE , "ice" ), + entry(MapColor.METAL , "iron" ), + entry(MapColor.PLANT , "foliage" ), + entry(MapColor.SNOW , "snow" ), + entry(MapColor.CLAY , "clay" ), + entry(MapColor.DIRT , "dirt" ), + entry(MapColor.STONE , "stone" ), + entry(MapColor.WATER , "water" ), + entry(MapColor.WOOD , "wood" ), + entry(MapColor.QUARTZ , "quartz" ), + entry(MapColor.COLOR_ORANGE , "adobe" ), + entry(MapColor.COLOR_MAGENTA , "magenta" ), + entry(MapColor.COLOR_LIGHT_BLUE, "light_blue"), + entry(MapColor.COLOR_YELLOW , "yellow" ), + entry(MapColor.COLOR_LIGHT_GREEN , "lime" ), + entry(MapColor.COLOR_PINK , "pink" ), + entry(MapColor.COLOR_GRAY , "gray" ), + entry(MapColor.COLOR_LIGHT_GRAY, "light_gray"), + entry(MapColor.COLOR_CYAN , "cyan" ), + entry(MapColor.COLOR_PURPLE , "purple" ), + entry(MapColor.COLOR_BLUE , "blue" ), + entry(MapColor.COLOR_BROWN , "brown" ), + entry(MapColor.COLOR_GREEN , "green" ), + entry(MapColor.COLOR_RED , "red" ), + entry(MapColor.COLOR_BLACK , "black" ), + entry(MapColor.GOLD , "gold" ), + entry(MapColor.DIAMOND , "diamond" ), + entry(MapColor.LAPIS , "lapis" ), + entry(MapColor.EMERALD , "emerald" ), + entry(MapColor.PODZOL , "obsidian" ), + entry(MapColor.NETHER , "netherrack"), //TODO fix these + entry(MapColor.TERRACOTTA_WHITE , "white_terracotta" ), + entry(MapColor.TERRACOTTA_ORANGE , "orange_terracotta" ), + entry(MapColor.TERRACOTTA_MAGENTA , "magenta_terracotta" ), + entry(MapColor.TERRACOTTA_LIGHT_BLUE, "light_blue_terracotta" ), + entry(MapColor.TERRACOTTA_YELLOW , "yellow_terracotta" ), + entry(MapColor.TERRACOTTA_LIGHT_GREEN , "lime_terracotta" ), + entry(MapColor.TERRACOTTA_PINK , "pink_terracotta" ), + entry(MapColor.TERRACOTTA_GRAY , "gray_terracotta" ), + entry(MapColor.TERRACOTTA_LIGHT_GRAY, "light_gray_terracotta" ), + entry(MapColor.TERRACOTTA_CYAN , "cyan_terracotta" ), + entry(MapColor.TERRACOTTA_PURPLE , "purple_terracotta" ), + entry(MapColor.TERRACOTTA_BLUE , "blue_terracotta" ), + entry(MapColor.TERRACOTTA_BROWN , "brown_terracotta" ), + entry(MapColor.TERRACOTTA_GREEN , "green_terracotta" ), + entry(MapColor.TERRACOTTA_RED , "red_terracotta" ), + entry(MapColor.TERRACOTTA_BLACK , "black_terracotta" ), + entry(MapColor.CRIMSON_NYLIUM , "crimson_nylium" ), + entry(MapColor.CRIMSON_STEM , "crimson_stem" ), + entry(MapColor.CRIMSON_HYPHAE , "crimson_hyphae" ), + entry(MapColor.WARPED_NYLIUM , "warped_nylium" ), + entry(MapColor.WARPED_STEM , "warped_stem" ), + entry(MapColor.WARPED_HYPHAE , "warped_hyphae" ), + entry(MapColor.WARPED_WART_BLOCK , "warped_wart" ), + entry(MapColor.DEEPSLATE , "deepslate" ), + entry(MapColor.RAW_IRON , "raw_iron" ), + entry(MapColor.GLOW_LICHEN , "glow_lichen" ) + ); +} diff --git a/src/main/java/carpet/script/utils/EntityTools.java b/src/main/java/carpet/script/utils/EntityTools.java new file mode 100644 index 0000000..5849e50 --- /dev/null +++ b/src/main/java/carpet/script/utils/EntityTools.java @@ -0,0 +1,32 @@ +package carpet.script.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +public class EntityTools +{ + /** + * Not a replacement for living entity jump() - this barely is to allow other entities that can't jump in vanilla to 'jump' + */ + public static void genericJump(Entity e) + { + if (!e.onGround() && !e.isInWaterOrBubble() && !e.isInLava()) + { + return; + } + float m = e.level().getBlockState(e.blockPosition()).getBlock().getJumpFactor(); + float g = e.level().getBlockState(BlockPos.containing(e.getX(), e.getBoundingBox().minY - 0.5000001D, e.getZ())).getBlock().getJumpFactor(); + float jumpVelocityMultiplier = m == 1.0D ? g : m; + float jumpStrength = (0.42F * jumpVelocityMultiplier); + Vec3 vec3d = e.getDeltaMovement(); + e.setDeltaMovement(vec3d.x, jumpStrength, vec3d.z); + if (e.isSprinting()) + { + float u = e.getYRot() * 0.017453292F; // yaw + e.setDeltaMovement(e.getDeltaMovement().add((-Mth.sin(g) * 0.2F), 0.0D, (Mth.cos(u) * 0.2F))); + } + e.hasImpulse = true; + } +} diff --git a/src/main/java/carpet/script/utils/EquipmentInventory.java b/src/main/java/carpet/script/utils/EquipmentInventory.java new file mode 100644 index 0000000..1be44ad --- /dev/null +++ b/src/main/java/carpet/script/utils/EquipmentInventory.java @@ -0,0 +1,131 @@ +package carpet.script.utils; + +import java.util.List; + +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public class EquipmentInventory implements Container +{ + private static final List slotToSlot = List.of( + EquipmentSlot.MAINHAND, + EquipmentSlot.FEET, EquipmentSlot.LEGS, EquipmentSlot.CHEST, EquipmentSlot.HEAD, + EquipmentSlot.OFFHAND + ); + + LivingEntity mob; + + public EquipmentInventory(LivingEntity mob) + { + this.mob = mob; + } + + @Override + public int getContainerSize() + { + return 6; + } + + @Override + public boolean isEmpty() + { + for (EquipmentSlot slot : slotToSlot) + { + if (!mob.getItemBySlot(slot).isEmpty()) + { + return false; + } + } + return true; + } + + @Override + public ItemStack getItem(int slot) + { + EquipmentSlot slotSlot; + try + { + slotSlot = slotToSlot.get(slot); + } + catch (IndexOutOfBoundsException ignored) + { + //going out of the index should be really exceptional + return ItemStack.EMPTY; + } + return mob.getItemBySlot(slotSlot); + } + + @Override + public ItemStack removeItem(int slot, int amount) + { + EquipmentSlot slotSlot; + try + { + slotSlot = slotToSlot.get(slot); + } + catch (IndexOutOfBoundsException ignored) + { + //going out of the index should be really exceptional + return ItemStack.EMPTY; + } + return mob.getItemBySlot(slotSlot).split(amount); + } + + @Override + public ItemStack removeItemNoUpdate(int slot) + { + EquipmentSlot slotSlot; + try + { + slotSlot = slotToSlot.get(slot); + } + catch (IndexOutOfBoundsException ignored) + { + //going out of the index should be really exceptional + return ItemStack.EMPTY; + } + ItemStack previous = mob.getItemBySlot(slotSlot); + mob.setItemSlot(slotSlot, ItemStack.EMPTY); + return previous; + } + + @Override + public void setItem(int slot, ItemStack stack) + { + EquipmentSlot slotSlot; + try + { + slotSlot = slotToSlot.get(slot); + } + catch (IndexOutOfBoundsException ignored) + { + //going out of the index should be really exceptional + return; + } + mob.setItemSlot(slotSlot, stack); + } + + @Override + public void setChanged() + { + + } + + @Override + public boolean stillValid(Player player) + { + return false; + } + + @Override + public void clearContent() + { + for (EquipmentSlot slot : slotToSlot) + { + mob.setItemSlot(slot, ItemStack.EMPTY); + } + } +} diff --git a/src/main/java/carpet/script/utils/Experimental.java b/src/main/java/carpet/script/utils/Experimental.java new file mode 100644 index 0000000..79d4283 --- /dev/null +++ b/src/main/java/carpet/script/utils/Experimental.java @@ -0,0 +1,152 @@ +package carpet.script.utils; + +//import carpet.fakes.MinecraftServerInterface; +//import carpet.fakes.ServerWorldInterface; +import carpet.script.CarpetScriptServer; +import carpet.script.value.ListValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DynamicOps; +import net.minecraft.Util; +import net.minecraft.commands.Commands; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.WorldLoader; +import net.minecraft.server.WorldStem; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackRepository; +import net.minecraft.server.packs.resources.CloseableResourceManager; +import net.minecraft.server.packs.resources.MultiPackResourceManager; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.world.level.DataPackConfig; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.storage.DerivedLevelData; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.WorldData; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class Experimental +{ + /* + public static Value reloadOne(MinecraftServer server) + { + LevelStorageSource.LevelStorageAccess session = ((MinecraftServerInterface) server).getCMSession(); + DataPackConfig dataPackSettings = session.getDataPacks(); + PackRepository resourcePackManager = server.getPackRepository(); + DataPackConfig dataPackSettings2 = MinecraftServer.configurePackRepository(resourcePackManager, dataPackSettings == null ? DataPackConfig.DEFAULT : dataPackSettings, false); + + CarpetScriptServer.LOG.error("datapacks: {}", dataPackSettings2.getEnabled()); + + MinecraftServer.ReloadableResources serverRM = ((MinecraftServerInterface) server).getResourceManager(); + var resourceManager = serverRM.resourceManager(); + + + final RegistryAccess.Frozen currentRegistry = server.registryAccess(); + + ImmutableList packsToLoad = resourcePackManager.getAvailableIds().stream().map(server.getPackRepository()::getPack).filter(Objects::nonNull).map(Pack::open).collect(ImmutableList.toImmutableList()); + final CloseableResourceManager resources = new MultiPackResourceManager(PackType.SERVER_DATA, packsToLoad); + //ReloadableServerResources managers = ReloadableServerResources.loadResources(resources, currentRegistry, server.isDedicatedServer() ? Commands.CommandSelection.DEDICATED : Commands.CommandSelection.INTEGRATED, server.getFunctionCompilationLevel(), Util.backgroundExecutor(), Util.backgroundExecutor()).join(); + + //believe the other one will fillup based on the datapacks only. + //resources.close(); + + + //not sure its needed, but doesn't seem to have a negative effect and might be used in some custom shtuff + //serverRM.updateGlobals(); + DynamicOps dynamicOps = RegistryOps.create(NbtOps.INSTANCE, server.registryAccess());//, (ResourceManager) resourceManager); + + WorldData saveProperties = session.getDataTag(dynamicOps, dataPackSettings2, server.registryAccess().allElementsLifecycle()); + + //RegistryReadOps registryOps = RegistryReadOps.create(NbtOps.INSTANCE, serverRM.getResourceManager(), (RegistryAccess.RegistryHolder) server.registryAccess()); + //WorldData saveProperties = session.getDataTag(registryOps, dataPackSettings2); + if (saveProperties == null) return Value.NULL; + //session.backupLevelDataFile(server.getRegistryManager(), saveProperties); // no need + + // MinecraftServer.createWorlds + // save properties should now contain dimension settings + WorldGenSettings generatorOptions = saveProperties.worldGenSettings(); + boolean bl = generatorOptions.isDebug(); + long l = generatorOptions.seed(); + long m = BiomeManager.obfuscateSeed(l); + Map, ServerLevel> existing_worlds = ((MinecraftServerInterface) server).getCMWorlds(); + List addeds = new ArrayList<>(); + for (Map.Entry, LevelStem> entry : generatorOptions.dimensions().entrySet()) + { + ResourceKey registryKey = entry.getKey(); + CarpetScriptServer.LOG.error("Analysing workld: {}", registryKey.location()); + if (!existing_worlds.containsKey(registryKey)) + { + addeds.add(ValueConversions.of(registryKey.location())); + ResourceKey registryKey2 = ResourceKey.create(Registry.DIMENSION_REGISTRY, registryKey.location()); + Holder holder2 = (entry.getValue()).typeHolder(); + ChunkGenerator chunkGenerator3 = entry.getValue().generator(); + DerivedLevelData unmodifiableLevelProperties = new DerivedLevelData(saveProperties, ((ServerWorldInterface) server.overworld()).getWorldPropertiesCM()); + ServerLevel serverWorld2 = new ServerLevel(server, Util.backgroundExecutor(), session, unmodifiableLevelProperties, registryKey2, entry.getValue(), WorldTools.NOOP_LISTENER, bl, m, ImmutableList.of(), false); + server.overworld().getWorldBorder().addListener(new BorderChangeListener.DelegateBorderChangeListener(serverWorld2.getWorldBorder())); + existing_worlds.put(registryKey2, serverWorld2); + } + } + return ListValue.wrap(addeds); + } + + public static Value reloadTwo(MinecraftServer server) + { + LevelStorageSource.LevelStorageAccess session = ((MinecraftServerInterface)server).getCMSession(); + DataPackConfig dataPackSettings = session.getDataPacks(); + PackRepository resourcePackManager = server.getPackRepository(); + + WorldLoader.InitConfig initConfig = new WorldLoader.InitConfig(new WorldLoader.PackConfig(resourcePackManager, dataPackSettings == null ? DataPackConfig.DEFAULT : dataPackSettings, false), Commands.CommandSelection.DEDICATED, 4); + + + final WorldStem stem = WorldLoader.load(initConfig, (resourceManager, dataPackConfigx) -> { + RegistryAccess.Writable writable = RegistryAccess.builtinCopy(); + DynamicOps dynamicOps = RegistryOps.createAndLoad(NbtOps.INSTANCE, writable, (ResourceManager) resourceManager); + WorldData worldData = session.getDataTag(dynamicOps, dataPackConfigx, writable.allElementsLifecycle()); + return Pair.of(worldData, writable.freeze()); + }, WorldStem::new, Util.backgroundExecutor(), Runnable::run).join(); + WorldGenSettings generatorOptions = stem.worldData().worldGenSettings(); + + boolean bl = generatorOptions.isDebug(); + long l = generatorOptions.seed(); + long m = BiomeManager.obfuscateSeed(l); + Map, ServerLevel> existing_worlds = ((MinecraftServerInterface)server).getCMWorlds(); + List addeds = new ArrayList<>(); + for (Map.Entry, LevelStem> entry : generatorOptions.dimensions().entrySet()) { + ResourceKey registryKey = entry.getKey(); + if (!existing_worlds.containsKey(registryKey)) + { + ResourceKey resourceKey2 = ResourceKey.create(Registry.DIMENSION_REGISTRY, registryKey.location()); + DerivedLevelData derivedLevelData = new DerivedLevelData(stem.worldData(), ((ServerWorldInterface) server.overworld()).getWorldPropertiesCM()); + ServerLevel serverLevel2 = new ServerLevel(server, Util.backgroundExecutor(), session, derivedLevelData, resourceKey2, entry.getValue(), WorldTools.NOOP_LISTENER, bl, m, ImmutableList.of(), false); + server.overworld().getWorldBorder().addListener(new BorderChangeListener.DelegateBorderChangeListener(serverLevel2.getWorldBorder())); + existing_worlds.put(resourceKey2, serverLevel2); + addeds.add(ValueConversions.of(registryKey.location())); + } + } + ((MinecraftServerInterface)server).reloadAfterReload(stem.registryAccess()); + return ListValue.wrap(addeds); + } + + */ +} diff --git a/src/main/java/carpet/script/utils/FeatureGenerator.java b/src/main/java/carpet/script/utils/FeatureGenerator.java new file mode 100644 index 0000000..32f84a3 --- /dev/null +++ b/src/main/java/carpet/script/utils/FeatureGenerator.java @@ -0,0 +1,475 @@ +package carpet.script.utils; + +import carpet.script.CarpetScriptServer; +import carpet.script.external.Vanilla; +import com.google.common.collect.ImmutableList; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.function.Function; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.Pools; +import net.minecraft.data.worldgen.ProcessorLists; +import net.minecraft.data.worldgen.placement.PlacementUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BiomeTags; +import net.minecraft.util.RandomSource; +import net.minecraft.util.valueproviders.ConstantInt; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.VerticalAnchor; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.FeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.configurations.SimpleRandomFeatureConfiguration; +import net.minecraft.world.level.levelgen.feature.configurations.TreeConfiguration; +import net.minecraft.world.level.levelgen.feature.featuresize.TwoLayersFeatureSize; +import net.minecraft.world.level.levelgen.feature.foliageplacers.BlobFoliagePlacer; +import net.minecraft.world.level.levelgen.feature.foliageplacers.FancyFoliagePlacer; +import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider; +import net.minecraft.world.level.levelgen.heightproviders.ConstantHeight; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureType; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.feature.treedecorators.BeehiveDecorator; +import net.minecraft.world.level.levelgen.feature.trunkplacers.FancyTrunkPlacer; +import net.minecraft.world.level.levelgen.feature.trunkplacers.StraightTrunkPlacer; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import net.minecraft.world.level.levelgen.structure.structures.JigsawStructure; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureProcessorList; + +import javax.annotation.Nullable; + +public class FeatureGenerator +{ + @Nullable + public static synchronized Boolean plop(String featureName, ServerLevel world, BlockPos pos) + { + Function custom = featureMap.get(featureName); + if (custom != null) + { + return custom.apply(world).plop(world, pos); + } + ResourceLocation id = ResourceLocation.parse(featureName); + Structure structure = world.registryAccess().registryOrThrow(Registries.STRUCTURE).get(id); + if (structure != null) + { + return plopAnywhere(structure, world, pos, world.getChunkSource().getGenerator(), false); + } + + ConfiguredFeature configuredFeature = world.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).get(id); + if (configuredFeature != null) + { + ThreadLocal checks = Vanilla.skipGenerationChecks(world); + checks.set(true); + try + { + return configuredFeature.place(world, world.getChunkSource().getGenerator(), world.random, pos); + } + finally + { + checks.set(false); + } + } + Optional> structureType = world.registryAccess().registryOrThrow(Registries.STRUCTURE_TYPE).getOptional(id); + if (structureType.isPresent()) + { + Structure configuredStandard = getDefaultFeature(structureType.get(), world, pos); + if (configuredStandard != null) + { + return plopAnywhere(configuredStandard, world, pos, world.getChunkSource().getGenerator(), false); + } + } + Feature feature = world.registryAccess().registryOrThrow(Registries.FEATURE).get(id); + if (feature != null) + { + ConfiguredFeature configuredStandard = getDefaultFeature(feature, world, pos, true); + if (configuredStandard != null) + { + ThreadLocal checks = Vanilla.skipGenerationChecks(world); + checks.set(true); + try + { + return configuredStandard.place(world, world.getChunkSource().getGenerator(), world.random, pos); + } + finally + { + checks.set(false); + } + } + } + return null; + } + + @Nullable + public static Structure resolveConfiguredStructure(String name, ServerLevel world, BlockPos pos) + { + ResourceLocation id = ResourceLocation.parse(name); + Structure configuredStructureFeature = world.registryAccess().registryOrThrow(Registries.STRUCTURE).get(id); + if (configuredStructureFeature != null) + { + return configuredStructureFeature; + } + StructureType structureFeature = world.registryAccess().registryOrThrow(Registries.STRUCTURE_TYPE).get(id); + if (structureFeature == null) + { + return null; + } + return getDefaultFeature(structureFeature, world, pos); + } + + public static synchronized boolean plopGrid(Structure structureFeature, ServerLevel world, BlockPos pos) + { + return plopAnywhere(structureFeature, world, pos, world.getChunkSource().getGenerator(), true); + } + + @FunctionalInterface + private interface Thing + { + Boolean plop(ServerLevel world, BlockPos pos); + } + + private static Thing simplePlop(ConfiguredFeature feature) + { + return (w, p) -> { + ThreadLocal checks = Vanilla.skipGenerationChecks(w); + checks.set(true); + try + { + return feature.place(w, w.getChunkSource().getGenerator(), w.random, p); + } + finally + { + checks.set(false); + } + }; + } + + private static > Thing simplePlop(F feature, FC config) + { + return simplePlop(new ConfiguredFeature<>(feature, config)); + } + + private static Thing simpleTree(TreeConfiguration config) + { + //config.ignoreFluidCheck(); + return simplePlop(new ConfiguredFeature<>(Feature.TREE, config)); + } + + private static Thing spawnCustomStructure(Structure structure) + { + return setupCustomStructure(structure, false); + } + + private static Thing setupCustomStructure(Structure structure, boolean wireOnly) + { + return (w, p) -> plopAnywhere(structure, w, p, w.getChunkSource().getGenerator(), wireOnly); + } + + private static Structure getDefaultFeature(StructureType structure, ServerLevel world, BlockPos pos) + { + // would be nice to have a way to grab structures of this type for position + // TODO allow old types, like vaillage, or bastion + Holder existingBiome = world.getBiome(pos); + Structure result = null; + for (Structure confstr : world.registryAccess().registryOrThrow(Registries.STRUCTURE).entrySet().stream(). + filter(cS -> cS.getValue().type() == structure).map(Map.Entry::getValue).toList()) + { + result = confstr; + if (confstr.biomes().contains(existingBiome)) + { + return result; + } + } + return result; + } + + private static ConfiguredFeature getDefaultFeature(Feature feature, ServerLevel world, BlockPos pos, boolean tryHard) + { + List> configuredStepFeatures = world.getBiome(pos).value().getGenerationSettings().features(); + for (HolderSet step : configuredStepFeatures) + { + for (Holder provider : step) + { + if (provider.value().feature().value().feature() == feature) + { + return provider.value().feature().value(); + } + } + } + if (!tryHard) + { + return null; + } + return world.registryAccess().registryOrThrow(Registries.CONFIGURED_FEATURE).entrySet().stream(). + filter(cS -> cS.getValue().feature() == feature). + findFirst().map(Map.Entry::getValue).orElse(null); + } + + + public static StructureStart shouldStructureStartAt(ServerLevel world, BlockPos pos, Structure structure, boolean computeBox) + { + ServerChunkCache chunkSource = world.getChunkSource(); + RandomState seed = chunkSource.randomState(); + ChunkGenerator generator = chunkSource.getGenerator(); + ChunkGeneratorStructureState structureState = chunkSource.getGeneratorState(); + List structureConfig = structureState.getPlacementsForStructure(Holder.direct(structure)); + ChunkPos chunkPos = new ChunkPos(pos); + boolean couldPlace = structureConfig.stream().anyMatch(p -> p.isStructureChunk(structureState, chunkPos.x, chunkPos.z)); + if (!couldPlace) + { + return null; + } + + HolderSet structureBiomes = structure.biomes(); + + if (!computeBox) + { + //Holder genBiome = generator.getBiomeSource().getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ()), seed.sampler()); + if (structure.findValidGenerationPoint(new Structure.GenerationContext( + world.registryAccess(), generator, generator.getBiomeSource(), + seed, world.getStructureManager(), world.getSeed(), chunkPos, world, structureBiomes::contains + )).isPresent()) + { + return StructureStart.INVALID_START; + } + } + else + { + StructureStart filledStructure = structure.generate( + world.registryAccess(), generator, generator.getBiomeSource(), seed, world.getStructureManager(), + world.getSeed(), chunkPos, 0, world, structureBiomes::contains); + if (filledStructure != null && filledStructure.isValid()) + { + return filledStructure; + } + } + return null; + } + + private static TreeConfiguration.TreeConfigurationBuilder createTree(Block block, Block block2, int i, int j, int k, int l) + { + return new TreeConfiguration.TreeConfigurationBuilder(BlockStateProvider.simple(block), new StraightTrunkPlacer(i, j, k), BlockStateProvider.simple(block2), new BlobFoliagePlacer(ConstantInt.of(l), ConstantInt.of(0), 3), new TwoLayersFeatureSize(1, 0, 1)); + } + + public static final Map> featureMap = new HashMap<>() + {{ + + put("oak_bees", l -> simpleTree(createTree(Blocks.OAK_LOG, Blocks.OAK_LEAVES, 4, 2, 0, 2).ignoreVines().decorators(List.of(new BeehiveDecorator(1.00F))).build())); + put("fancy_oak_bees", l -> simpleTree((new TreeConfiguration.TreeConfigurationBuilder(BlockStateProvider.simple(Blocks.OAK_LOG), new FancyTrunkPlacer(3, 11, 0), BlockStateProvider.simple(Blocks.OAK_LEAVES), new FancyFoliagePlacer(ConstantInt.of(2), ConstantInt.of(4), 4), new TwoLayersFeatureSize(0, 0, 0, OptionalInt.of(4)))).ignoreVines().decorators(List.of(new BeehiveDecorator(1.00F))).build())); + put("birch_bees", l -> simpleTree(createTree(Blocks.BIRCH_LOG, Blocks.BIRCH_LEAVES, 5, 2, 0, 2).ignoreVines().decorators(List.of(new BeehiveDecorator(1.00F))).build())); + + put("coral_tree", l -> simplePlop(Feature.CORAL_TREE, FeatureConfiguration.NONE)); + + put("coral_claw", l -> simplePlop(Feature.CORAL_CLAW, FeatureConfiguration.NONE)); + put("coral_mushroom", l -> simplePlop(Feature.CORAL_MUSHROOM, FeatureConfiguration.NONE)); + put("coral", l -> simplePlop(Feature.SIMPLE_RANDOM_SELECTOR, new SimpleRandomFeatureConfiguration(HolderSet.direct( + PlacementUtils.inlinePlaced(Feature.CORAL_TREE, FeatureConfiguration.NONE), + PlacementUtils.inlinePlaced(Feature.CORAL_CLAW, FeatureConfiguration.NONE), + PlacementUtils.inlinePlaced(Feature.CORAL_MUSHROOM, FeatureConfiguration.NONE) + )))); + put("bastion_remnant_units", l -> { + RegistryAccess regs = l.registryAccess(); + + HolderGetter processorLists = regs.lookupOrThrow(Registries.PROCESSOR_LIST); + Holder bastionGenericDegradation = processorLists.getOrThrow(ProcessorLists.BASTION_GENERIC_DEGRADATION); + + HolderGetter pools = regs.lookupOrThrow(Registries.TEMPLATE_POOL); + Holder empty = pools.getOrThrow(Pools.EMPTY); + + + return spawnCustomStructure( + new JigsawStructure(new Structure.StructureSettings( + l.registryAccess().registryOrThrow(Registries.BIOME).getOrCreateTag(BiomeTags.HAS_BASTION_REMNANT), + Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.NONE + ), + Holder.direct(new StructureTemplatePool( + empty, + ImmutableList.of( + Pair.of(StructurePoolElement.single("bastion/units/air_base", bastionGenericDegradation), 1) + ), + StructureTemplatePool.Projection.RIGID + )), + 6, + ConstantHeight.of(VerticalAnchor.absolute(33)), + false + ) + ); + }); + put("bastion_remnant_hoglin_stable", l -> { + RegistryAccess regs = l.registryAccess(); + + HolderGetter processorLists = regs.lookupOrThrow(Registries.PROCESSOR_LIST); + Holder bastionGenericDegradation = processorLists.getOrThrow(ProcessorLists.BASTION_GENERIC_DEGRADATION); + + HolderGetter pools = regs.lookupOrThrow(Registries.TEMPLATE_POOL); + Holder empty = pools.getOrThrow(Pools.EMPTY); + + return spawnCustomStructure( + new JigsawStructure(new Structure.StructureSettings( + l.registryAccess().registryOrThrow(Registries.BIOME).getOrCreateTag(BiomeTags.HAS_BASTION_REMNANT), + Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.NONE + ), + Holder.direct(new StructureTemplatePool( + empty, + ImmutableList.of( + Pair.of(StructurePoolElement.single("bastion/hoglin_stable/air_base", bastionGenericDegradation), 1) + ), + StructureTemplatePool.Projection.RIGID + )), + 6, + ConstantHeight.of(VerticalAnchor.absolute(33)), + false + ) + ); + }); + put("bastion_remnant_treasure", l -> { + RegistryAccess regs = l.registryAccess(); + + HolderGetter processorLists = regs.lookupOrThrow(Registries.PROCESSOR_LIST); + Holder bastionGenericDegradation = processorLists.getOrThrow(ProcessorLists.BASTION_GENERIC_DEGRADATION); + + HolderGetter pools = regs.lookupOrThrow(Registries.TEMPLATE_POOL); + Holder empty = pools.getOrThrow(Pools.EMPTY); + + return spawnCustomStructure( + new JigsawStructure(new Structure.StructureSettings( + l.registryAccess().registryOrThrow(Registries.BIOME).getOrCreateTag(BiomeTags.HAS_BASTION_REMNANT), + Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.NONE + ), + Holder.direct(new StructureTemplatePool( + empty, + ImmutableList.of( + Pair.of(StructurePoolElement.single("bastion/treasure/big_air_full", bastionGenericDegradation), 1) + ), + StructureTemplatePool.Projection.RIGID + )), + 6, + ConstantHeight.of(VerticalAnchor.absolute(33)), + false + ) + ); + }); + put("bastion_remnant_bridge", l -> { + RegistryAccess regs = l.registryAccess(); + + HolderGetter processorLists = regs.lookupOrThrow(Registries.PROCESSOR_LIST); + Holder bastionGenericDegradation = processorLists.getOrThrow(ProcessorLists.BASTION_GENERIC_DEGRADATION); + + HolderGetter pools = regs.lookupOrThrow(Registries.TEMPLATE_POOL); + Holder empty = pools.getOrThrow(Pools.EMPTY); + + return spawnCustomStructure( + new JigsawStructure(new Structure.StructureSettings( + l.registryAccess().registryOrThrow(Registries.BIOME).getOrCreateTag(BiomeTags.HAS_BASTION_REMNANT), + Map.of(), + GenerationStep.Decoration.SURFACE_STRUCTURES, + TerrainAdjustment.NONE + ), + Holder.direct(new StructureTemplatePool( + empty, + ImmutableList.of( + Pair.of(StructurePoolElement.single("bastion/bridge/starting_pieces/entrance_base", bastionGenericDegradation), 1) + ), + StructureTemplatePool.Projection.RIGID + )), + 6, + ConstantHeight.of(VerticalAnchor.absolute(33)), + false + ) + ); + }); + }}; + + + public static boolean plopAnywhere(Structure structure, ServerLevel world, BlockPos pos, ChunkGenerator generator, boolean wireOnly) + { + ThreadLocal checks = Vanilla.skipGenerationChecks(world); + checks.set(true); + try + { + StructureStart start = structure.generate(world.registryAccess(), generator, generator.getBiomeSource(), world.getChunkSource().randomState(), world.getStructureManager(), world.getSeed(), new ChunkPos(pos), 0, world, b -> true); + if (start == StructureStart.INVALID_START) + { + return false; + } + RandomSource rand = RandomSource.create(world.getRandom().nextInt()); + int j = pos.getX() >> 4; + int k = pos.getZ() >> 4; + long chId = ChunkPos.asLong(j, k); + world.getChunk(j, k).setStartForStructure(structure, start); + world.getChunk(j, k).addReferenceForStructure(structure, chId); + + BoundingBox box = start.getBoundingBox(); + + if (!wireOnly) + { + Registry registry3 = world.registryAccess().registryOrThrow(Registries.STRUCTURE); + world.setCurrentlyGenerating(() -> { + Objects.requireNonNull(structure); + return registry3.getResourceKey(structure).map(Object::toString).orElseGet(structure::toString); + }); + start.placeInChunk(world, world.structureManager(), generator, rand, box, new ChunkPos(j, k)); + } + //structurestart.notifyPostProcessAt(new ChunkPos(j, k)); + int i = Math.max(box.getXSpan(), box.getZSpan()) / 16 + 1; + + //int i = getRadius(); + for (int k1 = j - i; k1 <= j + i; ++k1) + { + for (int l1 = k - i; l1 <= k + i; ++l1) + { + if (k1 == j && l1 == k) + { + continue; + } + if (box.intersects(k1 << 4, l1 << 4, (k1 << 4) + 15, (l1 << 4) + 15)) + { + world.getChunk(k1, l1).addReferenceForStructure(structure, chId); + } + } + } + } + catch (Exception booboo) + { + CarpetScriptServer.LOG.error("Unknown Exception while plopping structure: " + booboo, booboo); + return false; + } + finally + { + checks.set(false); + } + return true; + } +} diff --git a/src/main/java/carpet/script/utils/GlocalFlag.java b/src/main/java/carpet/script/utils/GlocalFlag.java new file mode 100644 index 0000000..05cced3 --- /dev/null +++ b/src/main/java/carpet/script/utils/GlocalFlag.java @@ -0,0 +1,75 @@ +package carpet.script.utils; + +import javax.annotation.Nullable; +import java.util.function.Supplier; + +public class GlocalFlag extends ThreadLocal +{ + private final boolean initial; + + public GlocalFlag(boolean initial) + { + this.initial = initial; + } + + @Override + public Boolean initialValue() + { + return initial; + } + + /** + * Allows to thread-safely wrap a call while disabling a global flag and setting it back up right after. + * + * @param action - callback to invoke when the wrapping is all setup + * @param - returned value of that action, whatever that might be + * @return result of the action + */ + public T getWhileDisabled(Supplier action) + { + return whileValueReturn(!initial, action); + } + + private T whileValueReturn(boolean what, Supplier action) + { + T result; + boolean previous; + synchronized (this) + { + previous = get(); + set(what); + } + try + { + result = action.get(); + } + finally + { + set(previous); + } + return result; + } + + @Nullable + public T runIfEnabled(Supplier action) + { + synchronized (this) + { + if (get() != initial) + { + return null; + } + set(!initial); + } + T result; + try + { + result = action.get(); + } + finally + { + set(initial); + } + return result; + } +} diff --git a/src/main/java/carpet/script/utils/InputValidator.java b/src/main/java/carpet/script/utils/InputValidator.java new file mode 100644 index 0000000..f33edc3 --- /dev/null +++ b/src/main/java/carpet/script/utils/InputValidator.java @@ -0,0 +1,34 @@ +package carpet.script.utils; + +import carpet.script.exception.InternalExpressionException; + +import java.util.Locale; + +import net.minecraft.ResourceLocationException; +import net.minecraft.resources.ResourceLocation; + +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; + } + + public static ResourceLocation identifierOf(String string) + { + try + { + return ResourceLocation.parse(string); + } + catch (ResourceLocationException iie) + { + throw new InternalExpressionException("Incorrect identifier format '" + string + "': " + iie.getMessage()); + } + } + +} diff --git a/src/main/java/carpet/script/utils/ParticleParser.java b/src/main/java/carpet/script/utils/ParticleParser.java new file mode 100644 index 0000000..b386f52 --- /dev/null +++ b/src/main/java/carpet/script/utils/ParticleParser.java @@ -0,0 +1,43 @@ +package carpet.script.utils; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.arguments.ParticleArgument; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +public class ParticleParser +{ + private static final Map particleCache = new HashMap<>(); // we reset this on reloads, but probably need something better + + private static ParticleOptions parseParticle(String name, RegistryAccess lookup) + { + try + { + return ParticleArgument.readParticle(new StringReader(name), lookup); + } + catch (CommandSyntaxException e) + { + throw new IllegalArgumentException("No such particle: " + name); + } + } + + @Nullable + public static ParticleOptions getEffect(@Nullable String name, RegistryAccess lookup) + { + if (name == null) + { + return null; + } + return particleCache.computeIfAbsent(name, particle -> parseParticle(particle, lookup)); + } + + public static void resetCache() + { + particleCache.clear(); + } +} diff --git a/src/main/java/carpet/script/utils/PerlinNoiseSampler.java b/src/main/java/carpet/script/utils/PerlinNoiseSampler.java new file mode 100644 index 0000000..3c3daa7 --- /dev/null +++ b/src/main/java/carpet/script/utils/PerlinNoiseSampler.java @@ -0,0 +1,198 @@ +package carpet.script.utils; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.util.Mth; + +import java.util.Map; +import java.util.Random; + +// extracted from import net.minecraft.util.math.noise.PerlinNoiseSampler +public class PerlinNoiseSampler +{ + protected static final int[][] gradients3d = new int[][]{ + {1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, + {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}, + {0, 1, 1}, {0, -1, 1}, {0, 1, -1}, {0, -1, -1}, + {1, 1, 0}, {0, -1, 1}, {-1, 1, 0}, {0, -1, -1} + }; + protected static final int[][] gradients2d = new int[][]{{1, 1}, {-1, 1}, {1, -1}, {-1, -1}}; + + + private final byte[] permutations; + public final double originX; + public final double originY; + public final double originZ; + public static PerlinNoiseSampler instance = new PerlinNoiseSampler(new Random(0)); + public static Map samplers = new Long2ObjectOpenHashMap<>(); + + public static PerlinNoiseSampler getPerlin(long aLong) + { + if (samplers.size() > 256) + { + samplers.clear(); + } + return samplers.computeIfAbsent(aLong, seed -> new PerlinNoiseSampler(new Random(seed))); + } + + public PerlinNoiseSampler(Random random) + { + this.originX = random.nextDouble() * 256.0D; + this.originY = random.nextDouble() * 256.0D; + this.originZ = random.nextDouble() * 256.0D; + this.permutations = new byte[256]; + + int j; + for (j = 0; j < 256; ++j) + { + this.permutations[j] = (byte) j; + } + for (j = 0; j < 256; ++j) + { + int k = random.nextInt(256 - j); + byte b = this.permutations[j]; + this.permutations[j] = this.permutations[j + k]; + this.permutations[j + k] = b; + } + } + //3D + public double sample3d(double x, double y, double z) + {//, double d, double e) { + double f = x + this.originX; + double g = y + this.originY; + double h = z + this.originZ; + int i = Mth.floor(f); + int j = Mth.floor(g); + int k = Mth.floor(h); + double l = f - (double) i; + double m = g - (double) j; + double n = h - (double) k; + double o = perlinFade(l); + double p = perlinFade(m); + double q = perlinFade(n); + //double t; + /* + if (d != 0.0D) { + double r = Math.min(e, m); + t = (double)Mth.floor(r / d) * d; + } else { + t = 0.0D; + }*/ + //return this.sample(i, j, k, l, m - t, n, o, p, q); + return this.sample3d(i, j, k, l, m, n, o, p, q) / 2 + 0.5; + } + + private double sample3d(int sectionX, int sectionY, int sectionZ, double localX, double localY, double localZ, double fadeLocalX, double fadeLocalY, double fadeLocalZ) + { + int i = this.getGradient(sectionX) + sectionY; + int j = this.getGradient(i) + sectionZ; + int k = this.getGradient(i + 1) + sectionZ; + int l = this.getGradient(sectionX + 1) + sectionY; + int m = this.getGradient(l) + sectionZ; + int n = this.getGradient(l + 1) + sectionZ; + double d = grad3d(this.getGradient(j), localX, localY, localZ); + double e = grad3d(this.getGradient(m), localX - 1.0D, localY, localZ); + double f = grad3d(this.getGradient(k), localX, localY - 1.0D, localZ); + double g = grad3d(this.getGradient(n), localX - 1.0D, localY - 1.0D, localZ); + double h = grad3d(this.getGradient(j + 1), localX, localY, localZ - 1.0D); + double o = grad3d(this.getGradient(m + 1), localX - 1.0D, localY, localZ - 1.0D); + double p = grad3d(this.getGradient(k + 1), localX, localY - 1.0D, localZ - 1.0D); + double q = grad3d(this.getGradient(n + 1), localX - 1.0D, localY - 1.0D, localZ - 1.0D); + return lerp3(fadeLocalX, fadeLocalY, fadeLocalZ, d, e, f, g, h, o, p, q); + } + + private static double grad3d(int hash, double x, double y, double z) + { + int i = hash & 15; + return dot3d(gradients3d[i], x, y, z); + } + + protected static double dot3d(int[] gArr, double x, double y, double z) + { + return gArr[0] * x + gArr[1] * y + gArr[2] * z; + } + + public static double lerp3(double deltaX, double deltaY, double deltaZ, double d, double e, double f, double g, double h, double i, double j, double k) + { + return lerp(deltaZ, lerp2(deltaX, deltaY, d, e, f, g), lerp2(deltaX, deltaY, h, i, j, k)); + } + + //2D + public double sample2d(double x, double y) + { + double f = x + this.originX; + double g = y + this.originY; + int i = Mth.floor(f); + int j = Mth.floor(g); + double l = f - (double) i; + double m = g - (double) j; + double o = perlinFade(l); + double p = perlinFade(m); + return this.sample2d(i, j, l, m, o, p) / 2 + 0.5; + } + + private double sample2d(int sectionX, int sectionY, double localX, double localY, double fadeLocalX, double fadeLocalY) + { + int j = this.getGradient(sectionX) + sectionY; + int m = this.getGradient(sectionX + 1) + sectionY; + double d = grad2d(this.getGradient(j), localX, localY); + double e = grad2d(this.getGradient(m), localX - 1.0D, localY); + double f = grad2d(this.getGradient(j + 1), localX, localY - 1.0D); + double g = grad2d(this.getGradient(m + 1), localX - 1.0D, localY - 1.0D); + + return lerp2(fadeLocalX, fadeLocalY, d, e, f, g); + } + + private static double grad2d(int hash, double x, double y) + { + int i = hash & 3; + return dot2d(gradients2d[i], x, y); + } + + protected static double dot2d(int[] gArr, double x, double y) + { + return gArr[0] * x + gArr[1] * y; + } + + public static double lerp2(double deltaX, double deltaY, double d, double e, double f, double g) + { + return lerp(deltaY, lerp(deltaX, d, e), lerp(deltaX, f, g)); + } + + // 1D + public double sample1d(double x) + { + double f = x + this.originX; + int i = Mth.floor(f); + double l = f - i; + double o = perlinFade(l); + return this.sample1d(i, l, o) + 0.5; + } + + private double sample1d(int sectionX, double localX, double fadeLocalX) + { + double d = grad1d(this.getGradient(sectionX), localX); + double e = grad1d(this.getGradient(sectionX + 1), localX - 1.0D); + return lerp(fadeLocalX, d, e); + } + + private static double grad1d(int hash, double x) + { + return ((hash & 1) == 0) ? x : -x; + } + + public static double lerp(double delta, double first, double second) + { + return first + delta * (second - first); + } + + // shared + public int getGradient(int hash) + { + return this.permutations[hash & 255] & 255; + } + + public static double perlinFade(double d) + { + return d * d * d * (d * (d * 6.0D - 15.0D) + 10.0D); + } +} diff --git a/src/main/java/carpet/script/utils/ScarpetJsonDeserializer.java b/src/main/java/carpet/script/utils/ScarpetJsonDeserializer.java new file mode 100644 index 0000000..39e6387 --- /dev/null +++ b/src/main/java/carpet/script/utils/ScarpetJsonDeserializer.java @@ -0,0 +1,79 @@ +package carpet.script.utils; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; + +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; + +public class ScarpetJsonDeserializer implements JsonDeserializer +{ + + @Override + public Value deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + return parseElement(json); + } + + private Value parseElement(JsonElement element) throws JsonParseException + { + if (element.isJsonObject()) + { + return parseMap(element.getAsJsonObject()); + } + else if (element.isJsonArray()) + { + return parseList(element.getAsJsonArray()); + } + else if (element.isJsonPrimitive()) + { + return parsePrimitive(element.getAsJsonPrimitive()); + } + return Value.NULL; + } + + private Value parseMap(JsonObject jsonMap) throws JsonParseException + { + Map map = new HashMap<>(); + jsonMap.entrySet().forEach(entry -> map.put(new StringValue(entry.getKey()), parseElement(entry.getValue()))); + return MapValue.wrap(map); + } + + private Value parseList(JsonArray jsonList) throws JsonParseException + { + List list = new ArrayList<>(); + jsonList.forEach(elem -> list.add(parseElement(elem))); + return new ListValue(list); + } + + private Value parsePrimitive(JsonPrimitive primitive) throws JsonParseException + { + if (primitive.isString()) + { + return new StringValue(primitive.getAsString()); + } + else if (primitive.isBoolean()) + { + return primitive.getAsBoolean() ? Value.TRUE : Value.FALSE; + } + else if (primitive.isNumber()) + { + return NumericValue.of(primitive.getAsNumber()); + } + return Value.NULL; + } +} 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 0000000..ab622bd --- /dev/null +++ b/src/main/java/carpet/script/utils/ShapeDispatcher.java @@ -0,0 +1,2264 @@ +package carpet.script.utils; + +import carpet.script.CarpetScriptServer; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.language.Sys; +import carpet.script.utils.shapes.ShapeDirection; +import carpet.script.value.AbstractListValue; +import carpet.script.value.BlockValue; +import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NBTSerializableValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; +import carpet.script.value.Value; +import carpet.script.value.ValueConversions; + +import com.google.common.collect.Sets; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.ByteTag; +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.NbtUtils; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static java.util.Map.entry; + +public class ShapeDispatcher +{ + public record ShapeWithConfig(ExpiringShape shape, Map config) + { + } + + public static ShapeWithConfig fromFunctionArgs( + MinecraftServer server, ServerLevel world, + List lv, + Set playerSet + ) + { + 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 final MapValue map) + { + params = new HashMap<>(); + map.getMap().forEach((key, value) -> params.put(key.getString(), value)); + } + else if (paramValue instanceof final ListValue list) + { + params = parseParams(list.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(world.dimension().location().toString())); + params.putIfAbsent("duration", duration); + + if (params.containsKey("player")) + { + Value players = params.get("player"); + List playerVals; + if (players instanceof final ListValue list) + { + playerVals = list.getItems(); + } + else + { + playerVals = Collections.singletonList(players); + } + for (Value pVal : playerVals) + { + ServerPlayer player = EntityValue.getPlayerByValue(server, pVal); + if (player == null) + { + throw new InternalExpressionException("'player' parameter needs to represent an existing player, not " + pVal.getString()); + } + playerSet.add(player); + } + params.remove("player"); + } + return new ShapeWithConfig(ShapeDispatcher.create(server, shapeType, params), params); + } + + public static void sendShape(Collection players, List shapes, RegistryAccess regs) + { + List clientPlayers = new ArrayList<>(); + List alternativePlayers = new ArrayList<>(); + for (ServerPlayer player : players) + { + (Carpet.isValidCarpetPlayer(player) ? clientPlayers : alternativePlayers).add(player); + } + if (!clientPlayers.isEmpty()) + { + ListTag tag = new ListTag(); + int tagcount = 0; + for (ShapeWithConfig s : shapes) + { + tag.add(ExpiringShape.toTag(s.config(), regs)); // 4000 shapes limit boxes + if (tagcount++ > 1000) + { + tagcount = 0; + Tag finalTag = tag; + clientPlayers.forEach(p -> Vanilla.sendScarpetShapesDataToPlayer(p, finalTag)); + tag = new ListTag(); + } + } + Tag finalTag = tag; + if (!tag.isEmpty()) + { + clientPlayers.forEach(p -> Vanilla.sendScarpetShapesDataToPlayer(p, finalTag)); + } + } + if (!alternativePlayers.isEmpty()) + { + List> alternatives = new ArrayList<>(); + shapes.forEach(s -> alternatives.add(s.shape().alternative())); + alternativePlayers.forEach(p -> alternatives.forEach(a -> a.accept(p))); + } + } + + public static ParticleOptions getParticleData(String name, RegistryAccess regs) + { + try + { + return ParticleParser.getEffect(name, regs); + } + catch (IllegalArgumentException e) + { + throw new ThrowStatement(name, Throwables.UNKNOWN_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(MinecraftServer server, 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, server, userParams.get(key))); + }); + BiFunction, RegistryAccess, ExpiringShape> factory = ExpiringShape.shapeProviders.get(shapeType); + if (factory == null) + { + throw new InternalExpressionException("Unknown shape: " + shapeType); + } + return factory.apply(userParams, server.registryAccess()); + } + + // client + @Nullable + public static ExpiringShape fromTag(CompoundTag tag, Level level) + { + Map options = new HashMap<>(); + for (String key : tag.getAllKeys()) + { + Param decoder = Param.of.get(key); + if (decoder == null) + { + CarpetScriptServer.LOG.info("Unknown parameter for shape: " + key); + return null; + } + Value decodedValue = decoder.decode(tag.get(key), level); + options.put(key, decodedValue); + } + Value shapeValue = options.get("shape"); + if (shapeValue == null) + { + CarpetScriptServer.LOG.info("Shape id missing in " + String.join(", ", tag.getAllKeys())); + return null; + } + BiFunction, RegistryAccess, ExpiringShape> factory = ExpiringShape.shapeProviders.get(shapeValue.getString()); + if (factory == null) + { + CarpetScriptServer.LOG.info("Unknown shape: " + shapeValue.getString()); + return null; + } + try + { + return factory.apply(options, level.registryAccess()); + } + catch (InternalExpressionException exc) + { + CarpetScriptServer.LOG.info(exc.getMessage()); + } + return null; + } + + public abstract static class ExpiringShape + { + public static final Map, RegistryAccess, ExpiringShape>> shapeProviders = new HashMap<>() + {{ + put("line", creator(Line::new)); + put("box", creator(Box::new)); + put("sphere", creator(Sphere::new)); + put("cylinder", creator(Cylinder::new)); + put("label", creator(DisplayedText::new)); + put("polygon", creator(Polyface::new)); + put("block", creator(() -> new DisplayedSprite(false))); + put("item", creator(() -> new DisplayedSprite(true))); + }}; + + private static BiFunction, RegistryAccess, ExpiringShape> creator(Supplier shapeFactory) + { + return (o, regs) -> { + ExpiringShape shape = shapeFactory.get(); + shape.fromOptions(o, regs); + 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; + protected boolean discreteX, discreteY, discreteZ; + protected ResourceKey shapeDimension; + protected boolean debug; + + + protected ExpiringShape() + { + } + + public static CompoundTag toTag(Map params, RegistryAccess regs) + { + CompoundTag tag = new CompoundTag(); + params.forEach((k, v) -> { + Tag valTag = Param.of.get(k).toTag(v, regs); + if (valTag != null) + { + tag.put(k, valTag); + } + }); + return tag; + } + + private void fromOptions(Map options, RegistryAccess regs) + { + Set optionalParams = optionalParams(); + Set requiredParams = requiredParams(); + Set all = Sets.union(optionalParams, requiredParams); + if (!all.containsAll(options.keySet())) + { + throw new InternalExpressionException("Received unexpected parameters for shape: " + Sets.difference(options.keySet(), all)); + } + if (!options.keySet().containsAll(requiredParams)) + { + throw new InternalExpressionException("Missing required parameters for shape: " + Sets.difference(requiredParams, 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, regs); + } + + + protected void init(Map options, RegistryAccess regs) + { + + 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 = (fillColor >> 24 & 0xFF) / 255.0F; + this.fg = (fillColor >> 16 & 0xFF) / 255.0F; + this.fb = (fillColor >> 8 & 0xFF) / 255.0F; + this.fa = (fillColor & 0xFF) / 255.0F; + + color = NumericValue.asNumber(options.getOrDefault("color", optional.get("color"))).getInt(); + this.r = (color >> 24 & 0xFF) / 255.0F; + this.g = (color >> 16 & 0xFF) / 255.0F; + this.b = (color >> 8 & 0xFF) / 255.0F; + this.a = (color & 0xFF) / 255.0F; + + debug = false; + if (options.containsKey("debug")) + { + debug = options.get("debug").getBoolean(); + } + + key = 0; + followEntity = -1; + shapeDimension = ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(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"); + discreteX = snapTo.contains("dx"); + discreteY = snapTo.contains("dy"); + discreteZ = snapTo.contains("dz"); + } + } + + public int getExpiry() + { + return duration; + } + + public Vec3 toAbsolute(Entity e, Vec3 vec, float partialTick) + { + return vec.add( + snapX ? (discreteX ? Mth.floor(e.getX()) : Mth.lerp(partialTick, e.xo, e.getX())) : 0.0, + snapY ? (discreteY ? Mth.floor(e.getY()) : Mth.lerp(partialTick, e.yo, e.getY())) : 0.0, + snapZ ? (discreteZ ? Mth.floor(e.getZ()) : Mth.lerp(partialTick, e.zo, e.getZ())) : 0.0 + ); + } + + public Vec3 relativiseRender(Level world, Vec3 vec, float partialTick) + { + if (followEntity < 0) + { + return vec; + } + Entity e = world.getEntity(followEntity); + if (e == null) + { + return vec; + } + return toAbsolute(e, vec, partialTick); + } + + public Vec3 vecFromValue(Value value) + { + if (!(value instanceof final ListValue list)) + { + throw new InternalExpressionException("decoded value of " + value.getPrettyString() + " is not a triple"); + } + List elements = list.getItems(); + return new Vec3( + NumericValue.asNumber(elements.get(0)).getDouble(), + NumericValue.asNumber(elements.get(1)).getDouble(), + NumericValue.asNumber(elements.get(2)).getDouble() + ); + } + + protected ParticleOptions replacementParticle(RegistryAccess regs) + { + 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, regs); + } + + + public abstract Consumer alternative(); + + public long key(RegistryAccess regs) + { + if (key != 0) + { + return key; + } + key = calcKey(regs); + return key; + } + + protected long calcKey(RegistryAccess regs) + { // using FNV-1a algorithm + long hash = -3750763034362895579L; + hash ^= shapeDimension.hashCode(); + hash *= 1099511628211L; + hash ^= color; + hash *= 1099511628211L; + hash ^= followEntity; + hash *= 1099511628211L; + hash ^= Boolean.hashCode(debug); + hash *= 1099511628211L; + if (followEntity >= 0) + { + hash ^= snapTo.hashCode(); + hash *= 1099511628211L; + } + hash ^= Float.hashCode(lineWidth); + hash *= 1099511628211L; + if (fa != 0.0) + { + hash = 31 * hash + fillColor; + hash *= 1099511628211L; + } + return hash; + } + + private static final double xdif = new Random('x').nextDouble(); + private static final double ydif = new Random('y').nextDouble(); + private static final double zdif = new Random('z').nextDouble(); + + int vec3dhash(Vec3 vec) + { + return vec.add(xdif, ydif, zdif).hashCode(); + } + + // list of params that need to be there + private final Set required = Set.of("duration", "shape", "dim"); + private final Map optional = Map.of( + "color", new NumericValue(-1), + "follow", new NumericValue(-1), + "line", new NumericValue(2.0), + "debug", Value.FALSE, + "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 DisplayedText extends ExpiringShape + { + private final Set required = Set.of("pos", "text"); + private final Map optional = Map.ofEntries( + entry("facing", new StringValue("player")), + entry("raise", new NumericValue(0)), + entry("tilt", new NumericValue(0)), + entry("lean", new NumericValue(0)), + entry("turn", new NumericValue(0)), + entry("indent", new NumericValue(0)), + entry("height", new NumericValue(0)), + entry("align", new StringValue("center")), + entry("size", new NumericValue(10)), + entry("value", Value.NULL), + entry("doublesided", new NumericValue(0))); + + @Override + protected Set requiredParams() + { + return Sets.union(super.requiredParams(), required); + } + + @Override + protected Set optionalParams() + { + return Sets.union(super.optionalParams(), optional.keySet()); + } + + public DisplayedText() + { + } + + Vec3 pos; + String text; + int textcolor; + int textbck; + + ShapeDirection facing; + float raise; + float tilt; + float lean; + float turn; + float size; + float indent; + int align; + float height; + Component value; + boolean doublesided; + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + pos = vecFromValue(options.get("pos")); + value = ((FormattedTextValue) options.get("text")).getText(); + text = value.getString(); + if (options.containsKey("value")) + { + value = ((FormattedTextValue) options.get("value")).getText(); + } + textcolor = rgba2argb(color); + textbck = rgba2argb(fillColor); + String dir = options.getOrDefault("facing", optional.get("facing")).getString(); + facing = ShapeDirection.fromString(dir); + align = 0; + if (options.containsKey("align")) + { + String alignStr = options.get("align").getString(); + if ("right".equalsIgnoreCase(alignStr)) + { + align = 1; + } + else if ("left".equalsIgnoreCase(alignStr)) + { + align = -1; + } + } + doublesided = false; + if (options.containsKey("doublesided")) + { + doublesided = options.get("doublesided").getBoolean(); + } + + raise = NumericValue.asNumber(options.getOrDefault("raise", optional.get("raise"))).getFloat(); + tilt = NumericValue.asNumber(options.getOrDefault("tilt", optional.get("tilt"))).getFloat(); + lean = NumericValue.asNumber(options.getOrDefault("lean", optional.get("lean"))).getFloat(); + turn = NumericValue.asNumber(options.getOrDefault("turn", optional.get("turn"))).getFloat(); + indent = NumericValue.asNumber(options.getOrDefault("indent", optional.get("indent"))).getFloat(); + height = NumericValue.asNumber(options.getOrDefault("height", optional.get("height"))).getFloat(); + + size = NumericValue.asNumber(options.getOrDefault("size", optional.get("size"))).getFloat(); + } + + private int rgba2argb(int color) + { + int r = Math.max(1, color >> 24 & 0xFF); + int g = Math.max(1, color >> 16 & 0xFF); + int b = Math.max(1, color >> 8 & 0xFF); + int a = color & 0xFF; + return (a << 24) + (r << 16) + (g << 8) + b; + } + + @Override + public Consumer alternative() + { + return s -> { + }; + } + + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 5; + hash *= 1099511628211L; + hash ^= vec3dhash(pos); + hash *= 1099511628211L; + hash ^= text.hashCode(); + hash *= 1099511628211L; + if (facing != null) + { + hash ^= facing.hashCode(); + } + hash *= 1099511628211L; + hash ^= Float.hashCode(raise); + hash *= 1099511628211L; + hash ^= Float.hashCode(tilt); + hash *= 1099511628211L; + hash ^= Float.hashCode(lean); + hash *= 1099511628211L; + hash ^= Float.hashCode(turn); + hash *= 1099511628211L; + hash ^= Float.hashCode(indent); + hash *= 1099511628211L; + hash ^= Float.hashCode(height); + hash *= 1099511628211L; + hash ^= Float.hashCode(size); + hash *= 1099511628211L; + hash ^= Integer.hashCode(align); + hash *= 1099511628211L; + hash ^= Boolean.hashCode(doublesided); + hash *= 1099511628211L; + + return hash; + } + } + + public static class DisplayedSprite extends ExpiringShape + { + private final Set required = Set.of("pos"); + private final Map optional = Map.ofEntries( + entry("facing", new StringValue("north")), + entry("tilt", new NumericValue(0)), + entry("lean", new NumericValue(0)), + entry("turn", new NumericValue(0)), + entry("scale", ListValue.fromTriple(1, 1, 1)), + entry("blocklight", new NumericValue(-1)), + entry("skylight", new NumericValue(-1))); + private final boolean isitem; + + @Override + protected Set requiredParams() + { + return Sets.union(Sets.union(super.requiredParams(), required), Set.of(isitem ? "item" : "block")); + } + + @Override + protected Set optionalParams() + { + return Sets.union(Sets.union(super.optionalParams(), optional.keySet()), isitem ? Set.of("variant") : Set.of()); + } + + public DisplayedSprite(boolean i) + { + isitem = i; + } + + Vec3 pos; + + ShapeDirection facing; + + float tilt; + float lean; + float turn; + + int blockLight; + int skyLight; + + float scaleX = 1.0f; + float scaleY = 1.0f; + float scaleZ = 1.0f; + CompoundTag blockEntity; + BlockState blockState; + ItemStack item = null; + String itemTransformType; + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + pos = vecFromValue(options.get("pos")); + if (!this.isitem) + { + BlockValue block = (BlockValue) options.get("block"); + blockState = block.getBlockState(); + blockEntity = block.getData(); + } + else + { + this.item = ItemStack.parseOptional(regs, ((NBTSerializableValue) options.get("item")).getCompoundTag()); + } + blockLight = NumericValue.asNumber(options.getOrDefault("blocklight", optional.get("blocklight"))).getInt(); + if (blockLight > 15) + { + blockLight = 15; + } + skyLight = NumericValue.asNumber(options.getOrDefault("skylight", optional.get("skylight"))).getInt(); + if (skyLight > 15) + { + skyLight = 15; + } + + itemTransformType = "none"; + if (options.containsKey("variant")) + { + itemTransformType = options.get("variant").getString().toLowerCase(Locale.ROOT); + } + + String dir = options.getOrDefault("facing", optional.get("facing")).getString(); + facing = ShapeDirection.fromString(dir); + + tilt = NumericValue.asNumber(options.getOrDefault("tilt", optional.get("tilt"))).getFloat(); + lean = NumericValue.asNumber(options.getOrDefault("lean", optional.get("lean"))).getFloat(); + turn = NumericValue.asNumber(options.getOrDefault("turn", optional.get("turn"))).getFloat(); + List scale = ((ListValue) options.getOrDefault("scale", optional.get("scale"))).unpack(); + scaleY = NumericValue.asNumber(scale.get(1)).getFloat(); + scaleX = NumericValue.asNumber(scale.get(0)).getFloat(); + scaleZ = NumericValue.asNumber(scale.get(2)).getFloat(); + } + + @Override + public Consumer alternative() + { + return p -> { + ParticleOptions particle; + Registry blocks = p.getServer().registryAccess().registryOrThrow(Registries.BLOCK); + if (this.isitem) + { + if (Block.byItem(this.item.getItem()).defaultBlockState().isAir()) + { + return; + } + particle = getParticleData("block_marker " + blocks.getKey(Block.byItem(this.item.getItem())), p.level().registryAccess()); + } + else + { + particle = getParticleData("block_marker " + blocks.getKey(this.blockState.getBlock()), p.level().registryAccess()); + } + + Vec3 v = relativiseRender(p.level(), this.pos, 0); + p.serverLevel().sendParticles(p, particle, true, v.x, v.y, v.z, 1, 0.0, 0.0, 0.0, 0.0); + }; + } + + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 7; + hash *= 1099511628211L; + hash ^= Boolean.hashCode(isitem); + hash *= 1099511628211L; + hash ^= vec3dhash(pos); + hash *= 1099511628211L; + if (facing != null) + { + hash ^= facing.hashCode(); + } + hash *= 1099511628211L; + hash ^= Float.hashCode(tilt); + hash *= 1099511628211L; + hash ^= Float.hashCode(lean); + hash *= 1099511628211L; + hash ^= Float.hashCode(turn); + hash *= 1099511628211L; + hash ^= Float.hashCode(scaleY); + hash *= 1099511628211L; + hash ^= Float.hashCode(scaleZ); + hash *= 1099511628211L; + hash ^= Float.hashCode(scaleX); + hash *= 1099511628211L; + hash ^= Float.hashCode(skyLight); + hash *= 1099511628211L; + hash ^= Float.hashCode(blockLight); + hash *= 1099511628211L; + if (blockEntity != null) + { + hash ^= blockEntity.toString().hashCode(); + } + hash *= 1099511628211L; + if (blockState != null) + { + hash ^= blockState.hashCode(); + } + hash *= 1099511628211L; + hash ^= ValueConversions.of(item, regs).getString().hashCode(); + hash *= 1099511628211L; + hash ^= itemTransformType.hashCode(); + hash *= 1099511628211L; + + return hash; + } + } + + + public static class Box extends ExpiringShape + { + private final Set required = Set.of("from", "to"); + private final Map optional = Map.of(); + + @Override + protected Set requiredParams() + { + return Sets.union(super.requiredParams(), required); + } + + @Override + protected Set optionalParams() + { + return Sets.union(super.optionalParams(), optional.keySet()); + } + + public Box() + { + } + + Vec3 from; + Vec3 to; + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + from = vecFromValue(options.get("from")); + to = vecFromValue(options.get("to")); + } + + @Override + public Consumer alternative() + { + double density = Math.max(2.0, from.distanceTo(to) / 50 / (a + 0.1)); + return p -> + { + if (p.level().dimension() == shapeDimension) + { + particleMesh( + Collections.singletonList(p), + replacementParticle(p.level().registryAccess()), + density, + relativiseRender(p.level(), from, 0), + relativiseRender(p.level(), to, 0) + ); + } + }; + } + + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 1; + hash *= 1099511628211L; + hash ^= vec3dhash(from); + hash *= 1099511628211L; + hash ^= vec3dhash(to); + hash *= 1099511628211L; + return hash; + } + + public static int particleMesh(List playerList, ParticleOptions particle, double density, + Vec3 from, Vec3 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 Vec3(x1, y1, z1), new Vec3(x1, y2, z1), density) + + drawParticleLine(playerList, particle, new Vec3(x1, y2, z1), new Vec3(x2, y2, z1), density) + + drawParticleLine(playerList, particle, new Vec3(x2, y2, z1), new Vec3(x2, y1, z1), density) + + drawParticleLine(playerList, particle, new Vec3(x2, y1, z1), new Vec3(x1, y1, z1), density) + + + drawParticleLine(playerList, particle, new Vec3(x1, y1, z2), new Vec3(x1, y2, z2), density) + + drawParticleLine(playerList, particle, new Vec3(x1, y2, z2), new Vec3(x2, y2, z2), density) + + drawParticleLine(playerList, particle, new Vec3(x2, y2, z2), new Vec3(x2, y1, z2), density) + + drawParticleLine(playerList, particle, new Vec3(x2, y1, z2), new Vec3(x1, y1, z2), density) + + + drawParticleLine(playerList, particle, new Vec3(x1, y1, z1), new Vec3(x1, y1, z2), density) + + drawParticleLine(playerList, particle, new Vec3(x1, y2, z1), new Vec3(x1, y2, z2), density) + + drawParticleLine(playerList, particle, new Vec3(x2, y2, z1), new Vec3(x2, y2, z2), density) + + drawParticleLine(playerList, particle, new Vec3(x2, y1, z1), new Vec3(x2, y1, z2), density); + } + } + + public static class Polyface extends ExpiringShape + { + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 6; + hash *= 1099511628211L; + hash ^= mode; + hash *= 1099511628211L; + hash ^= relative.hashCode(); + hash *= 1099511628211L; + for (Vec3 i : vertexList) + { + hash ^= vec3dhash(i); + hash *= 1099511628211L; + } + hash ^= Boolean.hashCode(doublesided); + hash *= 1099511628211L; + hash ^= Integer.hashCode(vertexList.size()); + hash *= 1099511628211L; + hash ^= Boolean.hashCode(inneredges); + hash *= 1099511628211L; + return hash; + } + + ArrayList alterPoint = null; + final Random random = new Random(); + boolean doublesided; + + ArrayList getAlterPoint(ServerPlayer p) + { + if (alterPoint != null) + { + return alterPoint; + } + alterPoint = new ArrayList<>(); + switch (mode) + { + case 4: + for (int i = 0; i < vertexList.size(); i++) + { + Vec3 vecA = vertexList.get(i); + if (relative.get(i)) + { + vecA = relativiseRender(p.level(), vecA, 0); + } + i++; + Vec3 vecB = vertexList.get(i); + if (relative.get(i)) + { + vecB = relativiseRender(p.level(), vecB, 0); + } + i++; + Vec3 vecC = vertexList.get(i); + if (relative.get(i)) + { + vecC = relativiseRender(p.level(), vecC, 0); + } + alterDrawTriangles(vecA, vecB, vecC); + } + break; + case 6: + Vec3 vec0 = vertexList.get(0); + if (relative.get(0)) + { + vec0 = relativiseRender(p.level(), vec0, 0); + } + Vec3 vec1 = vertexList.get(1); + if (relative.get(1)) + { + vec1 = relativiseRender(p.level(), vec1, 0); + } + for (int i = 2; i < vertexList.size(); i++) + { + Vec3 vec = vertexList.get(i); + if (relative.get(i)) + { + vec = relativiseRender(p.level(), vec, 0); + } + alterDrawTriangles(vec0, vec1, vec); + vec1 = vec; + } + break; + case 5: + Vec3 vecA = vertexList.get(0); + if (relative.get(0)) + { + vecA = relativiseRender(p.level(), vecA, 0); + } + Vec3 vecB = vertexList.get(1); + if (relative.get(1)) + { + vecB = relativiseRender(p.level(), vecB, 0); + } + for (int i = 2; i < vertexList.size(); i++) + { + Vec3 vec = vertexList.get(i); + if (relative.get(i)) + { + vec = relativiseRender(p.level(), vec, 0); + } + alterDrawTriangles(vecA, vecB, vec); + vecA = vecB; + vecB = vec; + } + break; + default: + break; + } + + return alterPoint; + } + + void alterDrawTriangles(Vec3 a, Vec3 b, Vec3 c) + { + Vec3 bb = b.subtract(a); + Vec3 cc = c.subtract(a); + for (int i = 0; i / 8 < bb.cross(cc).length(); i++) + { + double x = random.nextDouble(); + double y = random.nextDouble(); + alterPoint.add(a.add(bb.scale(x / 2)).add(cc.scale(y / 2))); + if (x + y < 1) + { + alterPoint.add(a.add(bb.scale((x + 1) / 2)).add(cc.scale(y / 2))); + } + else + { + x = 1 - x; + y = 1 - y; + alterPoint.add(a.add(bb.scale(x / 2)).add(cc.scale((y + 1) / 2))); + } + } + } + + @Override + public Consumer alternative() + { + return p -> { + if (p.level().dimension() != this.shapeDimension) + { + return; + } + if (fa > 0.0f) + { + ParticleOptions locparticledata = getParticleData(String.format(Locale.ROOT, "dust %.1f %.1f %.1f %.1f", fr, fg, fb, fa), p.level().registryAccess()); + for (Vec3 v : getAlterPoint(p)) + { + p.serverLevel().sendParticles(p, locparticledata, true, + v.x, v.y, v.z, 1, + 0.0, 0.0, 0.0, 0.0); + } + } + }; + } + + private final Set required = Set.of("points"); + private final Map optional = Map.ofEntries( + entry("relative", Value.NULL), + entry("mode", new StringValue("polygon")), + entry("inner", Value.TRUE), + entry("doublesided", Value.TRUE) + ); + + @Override + protected Set requiredParams() + { + return Sets.union(super.requiredParams(), required); + } + + @Override + protected Set optionalParams() + { + return Sets.union(super.optionalParams(), optional.keySet()); + } + + ArrayList vertexList = new ArrayList<>(); + int mode; + ArrayList relative = new ArrayList<>(); + boolean inneredges; + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + + doublesided = options.getOrDefault("doublesided", optional.get("doublesided")).getBoolean(); + + if (options.get("points") instanceof final AbstractListValue abl) + { + abl.forEach(x -> vertexList.add(vecFromValue(x))); + } + String modeOption = options.getOrDefault("mode", optional.get("mode")).getString(); + inneredges = options.getOrDefault("inner", optional.get("inner")).getBoolean(); + if (vertexList.size() < 3) + { + throw new IllegalArgumentException("Unexpected vertex list size: " + vertexList.size()); + } + else if (vertexList.size() < 4) + { + inneredges = false; + } + if ("polygon".equals(modeOption)) + { + this.mode = 6; + } + else if ("strip".equals(modeOption)) + { + this.mode = 5; + } + else if ("triangles".equals(modeOption)) + { + this.mode = 4; + if (vertexList.size() % 3 != 0) + { + throw new IllegalArgumentException("Unexpected vertex list size: " + vertexList.size()); + } + } + if (options.getOrDefault("relative", optional.get("relative")) instanceof final AbstractListValue abl) + { + Iterator it = abl.iterator(); + for (long i = 0L; i < vertexList.size(); i++) + { + relative.add(it.hasNext() && it.next().getBoolean());//if part of it got defined. + } + } + else if (options.getOrDefault("relative", optional.get("relative")) instanceof final BooleanValue boolv) + { + for (long i = 0L; i < vertexList.size(); i++) + { + relative.add(boolv.getBoolean());//if it is a boolean. + } + } + else + { + for (long i = 0L; i < vertexList.size(); i++) + { + relative.add(true);//if there is nothing defined at all. + } + } + + } + } + + public static class Line extends ExpiringShape + { + private final Set required = Set.of("from", "to"); + private final Map optional = Map.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(); + } + + Vec3 from; + Vec3 to; + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + from = vecFromValue(options.get("from")); + to = vecFromValue(options.get("to")); + } + + @Override + public Consumer alternative() + { + double density = Math.max(2.0, from.distanceTo(to) / 50) / (a + 0.1); + return p -> + { + if (p.level().dimension() == shapeDimension) + { + drawParticleLine( + Collections.singletonList(p), + replacementParticle(p.level().registryAccess()), + relativiseRender(p.level(), from, 0), + relativiseRender(p.level(), to, 0), + density + ); + } + }; + } + + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 2; + hash *= 1099511628211L; + hash ^= vec3dhash(from); + hash *= 1099511628211L; + hash ^= vec3dhash(to); + hash *= 1099511628211L; + return hash; + } + } + + public static class Sphere extends ExpiringShape + { + private final Set required = Set.of("center", "radius"); + private final Map optional = Map.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(); + } + + Vec3 center; + float radius; + int level; + int subdivisions; + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + 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 -> + { + int partno = Math.min(1000, 20 * subdivisions); + RandomSource rand = p.level().getRandom(); + ServerLevel world = p.serverLevel(); + ParticleOptions particle = replacementParticle(world.registryAccess()); + + Vec3 ccenter = relativiseRender(world, center, 0); + + double ccx = ccenter.x; + double ccy = ccenter.y; + double ccz = ccenter.z; + + for (int i = 0; i < partno; i++) + { + float theta = (float) Math.asin(rand.nextDouble() * 2.0 - 1.0); + float phi = (float) (2 * Math.PI * rand.nextDouble()); + + double x = radius * Mth.cos(theta) * Mth.cos(phi); + double y = radius * Mth.cos(theta) * Mth.sin(phi); + double z = radius * Mth.sin(theta); + world.sendParticles(p, particle, true, + x + ccx, y + ccy, z + ccz, 1, + 0.0, 0.0, 0.0, 0.0); + } + }; + } + + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 3; + hash *= 1099511628211L; + hash ^= vec3dhash(center); + hash *= 1099511628211L; + hash ^= Double.hashCode(radius); + hash *= 1099511628211L; + hash ^= level; + hash *= 1099511628211L; + return hash; + } + } + + public static class Cylinder extends ExpiringShape + { + private final Set required = Set.of("center", "radius"); + private final Map optional = Map.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()); + } + + Vec3 center; + float height; + float radius; + int level; + int subdivisions; + Direction.Axis axis; + + private Cylinder() + { + super(); + } + + @Override + protected void init(Map options, RegistryAccess regs) + { + super.init(options, regs); + 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.byName(options.getOrDefault("axis", optional.get("axis")).getString()); + } + + + @Override + public Consumer alternative() + { + return p -> + { + int partno = (int) Math.min(1000, Math.sqrt(20 * subdivisions * (1 + height))); + RandomSource rand = p.level().getRandom(); + ServerLevel world = p.serverLevel(); + ParticleOptions particle = replacementParticle(world.registryAccess()); + + Vec3 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 < partno; i++) + { + float d = rand.nextFloat() * height; + float phi = (float) (2 * Math.PI * rand.nextDouble()); + double x = radius * Mth.cos(phi); + double y = d; + double z = radius * Mth.sin(phi); + world.sendParticles(p, particle, true, x + ccx, y + ccy, z + ccz, 1, 0.0, 0.0, 0.0, 0.0); + } + } + else if (axis == Direction.Axis.X) + { + for (int i = 0; i < partno; i++) + { + float d = rand.nextFloat() * height; + float phi = (float) (2 * Math.PI * rand.nextDouble()); + double x = d; + double y = radius * Mth.cos(phi); + double z = radius * Mth.sin(phi); + world.sendParticles(p, particle, true, x + ccx, y + ccy, z + ccz, 1, 0.0, 0.0, 0.0, 0.0); + } + } + else // Z + { + for (int i = 0; i < partno; i++) + { + float d = rand.nextFloat() * height; + float phi = (float) (2 * Math.PI * rand.nextDouble()); + double x = radius * Mth.sin(phi); + double y = radius * Mth.cos(phi); + double z = d; + world.sendParticles(p, particle, true, x + ccx, y + ccy, z + ccz, 1, 0.0, 0.0, 0.0, 0.0); + } + } + }; + } + + @Override + public long calcKey(RegistryAccess regs) + { + long hash = super.calcKey(regs); + hash ^= 4; + hash *= 1099511628211L; + hash ^= vec3dhash(center); + hash *= 1099511628211L; + hash ^= Double.hashCode(radius); + hash *= 1099511628211L; + hash ^= Double.hashCode(height); + hash *= 1099511628211L; + hash ^= level; + hash *= 1099511628211L; + return hash; + } + } + + + public abstract static class Param + { + public static final Map of = new HashMap<>() + {{ + put("mode", new StringChoiceParam("mode", "polygon", "strip", "triangles")); + put("relative", new OptionalBoolListParam("relative")); + put("inner", new BoolParam("inner")); + put("shape", new ShapeParam()); + put("dim", new DimensionParam()); + put("duration", new NonNegativeIntParam("duration")); + put("color", new ColorParam("color")); + put("follow", new EntityParam("follow")); + put("variant", new StringChoiceParam("variant", + "NONE", + "THIRD_PERSON_LEFT_HAND", + "THIRD_PERSON_RIGHT_HAND", + "FIRST_PERSON_LEFT_HAND", + "FIRST_PERSON_RIGHT_HAND", + "HEAD", + "GUI", + "GROUND", + "FIXED") + { + @Override + public Value validate(Map o, MinecraftServer s, Value v) + { + return super.validate(o, s, new StringValue(v.getString().toUpperCase(Locale.ROOT))); + } + }); + put("snap", new StringChoiceParam("snap", + "xyz", "xz", "yz", "xy", "x", "y", "z", + "dxdydz", "dxdz", "dydz", "dxdy", "dx", "dy", "dz", + "xdz", "dxz", "ydz", "dyz", "xdy", "dxy", + "xydz", "xdyz", "xdydz", "dxyz", "dxydz", "dxdyz" + )); + 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("pos", new Vec3Param("pos", false)); + put("radius", new PositiveFloatParam("radius")); + put("level", new PositiveIntParam("level")); + put("height", new FloatParam("height")); + put("width", new FloatParam("width")); + put("scale", new Vec3Param("scale", false) + { + @Override + public Value validate(java.util.Map options, MinecraftServer server, Value value) + { + if (value instanceof final NumericValue vn) + { + value = ListValue.of(vn, vn, vn); + } + return super.validate(options, server, value); + } + + }); + put("axis", new StringChoiceParam("axis", "x", "y", "z")); + put("points", new PointsParam("points")); + put("text", new FormattedTextParam("text")); + put("value", new FormattedTextParam("value")); + put("size", new PositiveIntParam("size")); + put("align", new StringChoiceParam("align", "center", "left", "right")); + + put("block", new BlockParam("block")); + put("item", new ItemParam("item")); + put("blocklight", new NonNegativeIntParam("blocklight")); + put("skylight", new NonNegativeIntParam("skylight")); + put("indent", new FloatParam("indent")); + put("raise", new FloatParam("raise")); + put("tilt", new FloatParam("tilt")); + put("lean", new FloatParam("lean")); + put("turn", new FloatParam("turn")); + put("facing", new StringChoiceParam("facing", "player", "camera", "north", "south", "east", "west", "up", "down")); + put("doublesided", new BoolParam("doublesided")); + put("debug", new BoolParam("debug")); + + }}; + protected String id; + + protected Param(String id) + { + this.id = id; + } + + @Nullable + public abstract Tag toTag(Value value, final RegistryAccess regs); //validates value, returning null if not necessary to keep it and serialize + + public abstract Value validate(Map options, MinecraftServer server, Value value); // makes sure the value is proper + + public abstract Value decode(Tag tag, Level level); + } + + public static class OptionalBoolListParam extends Param + { + public OptionalBoolListParam(String id) + { + super(id); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return value.toTag(true, regs); + } + + @Override + public Value decode(Tag tag, Level level) + { + if (tag instanceof final ListTag list) + { + return ListValue.wrap(list.stream().map(x -> BooleanValue.of(((NumericTag) x).getAsNumber().doubleValue() != 0))); + } + if (tag instanceof final ByteTag booltag) + { + return BooleanValue.of(booltag.getAsByte() != 0); + } + return Value.NULL; + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + if (value instanceof final AbstractListValue lv) + { + return ListValue.wrap(lv.unpack().stream().map(Value::getBoolean).map(BooleanValue::of)); + } + if (value instanceof BooleanValue || value.isNull()) + { + return value; + } + return BooleanValue.of(value.getBoolean()); + } + } + + public abstract static class StringParam extends Param + { + protected StringParam(String id) + { + super(id); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return StringTag.valueOf(value.getString()); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new StringValue(tag.getAsString()); + } + } + + public static class BlockParam extends Param + { + + protected BlockParam(String id) + { + super(id); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + if (value instanceof BlockValue) + { + return value; + } + return BlockValue.fromString(value.getString(), server.overworld()); + } + + @Nullable + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + if (value instanceof final BlockValue blv) + { + CompoundTag com = NbtUtils.writeBlockState(blv.getBlockState()); + CompoundTag dataTag = blv.getData(); + if (dataTag != null) + { + com.put("TileEntityData", dataTag); + } + return com; + } + return null; + } + + @Override + public Value decode(Tag tag, Level level) + { + BlockState bs = NbtUtils.readBlockState(level.holderLookup(Registries.BLOCK), (CompoundTag) tag); + CompoundTag compoundTag2 = null; + if (((CompoundTag) tag).contains("TileEntityData", 10)) + { + compoundTag2 = ((CompoundTag) tag).getCompound("TileEntityData"); + } + return new BlockValue(bs, compoundTag2); + } + } + + public static class ItemParam extends Param + { + + protected ItemParam(String id) + { + super(id); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + ItemStack item = ValueConversions.getItemStackFromValue(value, true, server.registryAccess()); + return new NBTSerializableValue(item.saveOptional(server.registryAccess())); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return ((NBTSerializableValue) value).getTag(); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new NBTSerializableValue(tag); + } + } + + public static class TextParam extends StringParam + { + protected TextParam(String id) + { + super(id); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + return value; + } + } + + public static class FormattedTextParam extends StringParam + { + + protected FormattedTextParam(String id) + { + super(id); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + if (!(value instanceof FormattedTextValue)) + { + value = new FormattedTextValue(Component.literal(value.getString())); + } + return value; + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + if (!(value instanceof FormattedTextValue)) + { + value = new FormattedTextValue(Component.literal(value.getString())); + } + return StringTag.valueOf(((FormattedTextValue) value).serialize(regs)); + } + + @Override + public Value decode(Tag tag, Level level) + { + return FormattedTextValue.deserialize(tag.getAsString(), level.registryAccess()); + } + } + + + public static class StringChoiceParam extends StringParam + { + private final Set options; + + public StringChoiceParam(String id, String... options) + { + super(id); + this.options = Sets.newHashSet(options); + } + + @Nullable + @Override + public Value validate(Map options, MinecraftServer server, 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, MinecraftServer server, Value value) + { + return value; + } + } + + public static class ShapeParam extends StringParam + { + protected ShapeParam() + { + super("shape"); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + String shape = value.getString(); + if (!ExpiringShape.shapeProviders.containsKey(shape)) + { + throw new InternalExpressionException("Unknown shape: " + shape); + } + return value; + } + } + + public abstract static class NumericParam extends Param + { + protected NumericParam(String id) + { + super(id); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + if (!(value instanceof NumericValue)) + { + throw new InternalExpressionException("'" + id + "' needs to be a number"); + } + return value; + } + } + + public static class BoolParam extends NumericParam + { + protected BoolParam(String id) + { + super(id); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return ByteTag.valueOf(value.getBoolean()); + } + + @Override + public Value decode(Tag tag, Level level) + { + return BooleanValue.of(((ByteTag) tag).getAsByte() > 0); + } + } + + public static class FloatParam extends NumericParam + { + protected FloatParam(String id) + { + super(id); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new NumericValue(((FloatTag) tag).getAsFloat()); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return FloatTag.valueOf(NumericValue.asNumber(value, id).getFloat()); + } + } + + public abstract static class PositiveParam extends NumericParam + { + protected PositiveParam(String id) + { + super(id); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + Value ret = super.validate(options, server, 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, Level level) + { + return new NumericValue(((FloatTag) tag).getAsFloat()); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return FloatTag.valueOf(NumericValue.asNumber(value, id).getFloat()); + } + + } + + public static class PositiveIntParam extends PositiveParam + { + protected PositiveIntParam(String id) + { + super(id); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new NumericValue(((IntTag) tag).getAsInt()); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return IntTag.valueOf(NumericValue.asNumber(value, id).getInt()); + } + + } + + public static class NonNegativeIntParam extends NumericParam + { + protected NonNegativeIntParam(String id) + { + super(id); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new NumericValue(((IntTag) tag).getAsInt()); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return IntTag.valueOf(NumericValue.asNumber(value, id).getInt()); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + Value ret = super.validate(options, server, 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, Level level) + { + return new NumericValue(((FloatTag) tag).getAsFloat()); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return FloatTag.valueOf(NumericValue.asNumber(value, id).getFloat()); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + Value ret = super.validate(options, server, value); + if (((NumericValue) ret).getDouble() < 0) + { + throw new InternalExpressionException("'" + id + "' should be non-negative"); + } + return ret; + } + } + + + public static class Vec3Param extends Param + { + private final boolean roundsUpForBlocks; + + protected Vec3Param(String id, boolean doesRoundUpForBlocks) + { + super(id); + roundsUpForBlocks = doesRoundUpForBlocks; + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + return validate(this, options, value, roundsUpForBlocks); + } + + public static Value validate(Param p, Map options, Value value, boolean roundsUp) + { + if (value instanceof final BlockValue bv) + { + 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 = bv.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 final ListValue list) + { + List values = list.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 final EntityValue ev) + { + 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 = ev.getEntity(); + return ListValue.of( + new NumericValue(e.getX()), + new NumericValue(e.getY()), + new NumericValue(e.getZ()) + ); + } + CarpetScriptServer.LOG.error("Value: " + value.getString()); + throw new InternalExpressionException("'" + p.id + "' requires a triple, block or entity to indicate position"); + } + + @Override + public Value decode(Tag tag, Level level) + { + 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, final RegistryAccess regs) + { + List lv = ((ListValue) value).getItems(); + ListTag tag = new ListTag(); + tag.add(DoubleTag.valueOf(NumericValue.asNumber(lv.get(0), "x").getDouble())); + tag.add(DoubleTag.valueOf(NumericValue.asNumber(lv.get(1), "y").getDouble())); + tag.add(DoubleTag.valueOf(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, MinecraftServer server, Value value) + { + if (!(value instanceof final ListValue list)) + { + throw new InternalExpressionException(id + " parameter should be a list"); + } + List points = new ArrayList<>(); + for (Value point : list.getItems()) + { + points.add(Vec3Param.validate(this, options, point, false)); + } + return ListValue.wrap(points); + } + + @Override + public Value decode(Tag tag, Level level) + { + ListTag ltag = (ListTag) tag; + List points = new ArrayList<>(); + for (int i = 0, ll = ltag.size(); i < ll; i++) + { + ListTag ptag = ltag.getList(i); + points.add(ListValue.of( + new NumericValue(ptag.getDouble(0)), + new NumericValue(ptag.getDouble(1)), + new NumericValue(ptag.getDouble(2)) + )); + } + return ListValue.wrap(points); + } + + @Override + public Tag toTag(Value pointsValue, final RegistryAccess regs) + { + List lv = ((ListValue) pointsValue).getItems(); + ListTag ltag = new ListTag(); + for (Value value : lv) + { + List coords = ((ListValue) value).getItems(); + ListTag tag = new ListTag(); + tag.add(DoubleTag.valueOf(NumericValue.asNumber(coords.get(0), "x").getDouble())); + tag.add(DoubleTag.valueOf(NumericValue.asNumber(coords.get(1), "y").getDouble())); + tag.add(DoubleTag.valueOf(NumericValue.asNumber(coords.get(2), "z").getDouble())); + ltag.add(tag); + } + return ltag; + } + } + + + public static class ColorParam extends NumericParam + { + protected ColorParam(String id) + { + super(id); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new NumericValue(((IntTag) tag).getAsInt()); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return IntTag.valueOf(NumericValue.asNumber(value, id).getInt()); + } + } + + public static class EntityParam extends Param + { + + protected EntityParam(String id) + { + super(id); + } + + @Override + public Tag toTag(Value value, final RegistryAccess regs) + { + return IntTag.valueOf(NumericValue.asNumber(value, id).getInt()); + } + + @Override + public Value validate(Map options, MinecraftServer server, Value value) + { + if (value instanceof final EntityValue ev) + { + return new NumericValue(ev.getEntity().getId()); + } + ServerPlayer player = EntityValue.getPlayerByValue(server, value); + if (player == null) + { + throw new InternalExpressionException(id + " parameter needs to represent an entity or player"); + } + return new NumericValue(player.getId()); + } + + @Override + public Value decode(Tag tag, Level level) + { + return new NumericValue(((IntTag) tag).getAsInt()); + } + } + + private static boolean isStraight(Vec3 from, Vec3 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, ParticleOptions particle, Vec3 from, Vec3 to, double density) + { + double distance = from.distanceTo(to); + int particles = (int) (distance / density); + Vec3 towards = to.subtract(from); + int parts = 0; + for (ServerPlayer player : playerList) + { + ServerLevel world = player.serverLevel(); + world.sendParticles(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.sendParticles(player, particle, true, + from.x, from.y, from.z, 1, 0.0, 0.0, 0.0, 0.0); + world.sendParticles(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 (ServerPlayer player : playerList) + { + ServerLevel world = player.serverLevel(); + world.sendParticles(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.sendParticles(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, ParticleOptions particle, Vec3 from, Vec3 to, double density) + { + double distance = from.distanceToSqr(to); + if (distance == 0) + { + return 0; + } + int pcount = 0; + if (distance < 100) + { + RandomSource rand = players.get(0).level().random; + int particles = (int) (distance / density) + 1; + Vec3 towards = to.subtract(from); + for (int i = 0; i < particles; i++) + { + Vec3 at = from.add(towards.scale(rand.nextDouble())); + for (ServerPlayer player : players) + { + player.serverLevel().sendParticles(player, particle, true, + at.x, at.y, at.z, 1, + 0.0, 0.0, 0.0, 0.0); + pcount++; + } + } + return pcount; + } + + if (isStraight(from, to, density)) + { + return drawOptimizedParticleLine(players, particle, from, to, density); + } + Vec3 incvec = to.subtract(from).scale(2 * density / Math.sqrt(distance)); + + for (Vec3 delta = new Vec3(0.0, 0.0, 0.0); + delta.lengthSqr() < distance; + delta = delta.add(incvec.scale(Sys.randomizer.nextFloat()))) + { + for (ServerPlayer player : players) + { + player.serverLevel().sendParticles(player, particle, true, + delta.x + from.x, delta.y + from.y, delta.z + from.z, 1, + 0.0, 0.0, 0.0, 0.0); + pcount++; + } + } + return pcount; + } +} diff --git a/src/main/java/carpet/script/utils/ShapesRenderer.java b/src/main/java/carpet/script/utils/ShapesRenderer.java new file mode 100644 index 0000000..7d6edd6 --- /dev/null +++ b/src/main/java/carpet/script/utils/ShapesRenderer.java @@ -0,0 +1,1435 @@ +package carpet.script.utils; + +import carpet.script.CarpetScriptServer; +import carpet.script.external.Carpet; +import carpet.script.external.VanillaClient; +import carpet.script.utils.shapes.ShapeDirection; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.BufferUploader; +import com.mojang.blaze3d.vertex.ByteBufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.mojang.blaze3d.vertex.VertexFormat.Mode; +import com.mojang.math.Axis; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.function.BiFunction; + +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.model.ShulkerModel; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.ItemBlockRenderTypes; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.MultiBufferSource; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.Sheets; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.resources.model.Material; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.Mth; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.LeavesBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.ShulkerBoxBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4f; +import org.joml.Matrix4fStack; + +public class ShapesRenderer +{ + private final Map, Long2ObjectOpenHashMap>> shapes; + private final Map, Long2ObjectOpenHashMap>> labels; + private final Minecraft client; + + private final Map>> renderedShapes + = new HashMap<>() + {{ + put("line", RenderedLine::new); + put("box", RenderedBox::new); + put("sphere", RenderedSphere::new); + put("cylinder", RenderedCylinder::new); + put("label", RenderedText::new); + put("polygon", RenderedPolyface::new); + put("block", (c, s) -> new RenderedSprite(c, s, false)); + put("item", (c, s) -> new RenderedSprite(c, s, true)); + }}; + + public static void rotatePoseStackByShapeDirection(PoseStack poseStack, ShapeDirection shapeDirection, Camera camera, Vec3 objectPos) + { + switch (shapeDirection) + { + case NORTH -> {} + case SOUTH -> poseStack.mulPose(Axis.YP.rotationDegrees(180)); + case EAST -> poseStack.mulPose(Axis.YP.rotationDegrees(270)); + case WEST -> poseStack.mulPose(Axis.YP.rotationDegrees(90)); + case UP -> poseStack.mulPose(Axis.XP.rotationDegrees(90)); + case DOWN -> poseStack.mulPose(Axis.XP.rotationDegrees(-90)); + case CAMERA -> poseStack.mulPose(camera.rotation()); + case PLAYER -> { + Vec3 vector = objectPos.subtract(camera.getPosition()); + double x = vector.x; + double y = vector.y; + double z = vector.z; + double d = Math.sqrt(x * x + z * z); + float rotX = (float) (Math.atan2(x, z)); + float rotY = (float) (Math.atan2(y, d)); + + // that should work somehow but it doesn't for some reason + //matrices.mulPose(new Quaternion( -rotY, rotX, 0, false)); + + poseStack.mulPose(Axis.YP.rotation(rotX)); + poseStack.mulPose(Axis.XP.rotation(-rotY)); + } + } + } + + public ShapesRenderer(Minecraft minecraftClient) + { + this.client = minecraftClient; + shapes = new HashMap<>(); + labels = new HashMap<>(); + } + + public void render(Matrix4f modelViewMatrix, Camera camera, float partialTick) + { + Runnable token = Carpet.startProfilerSection("Scarpet client"); + // posestack is not needed anymore - left as TODO to cleanup later + PoseStack matrices = new PoseStack(); + + //Camera camera = this.client.gameRenderer.getCamera(); + ClientLevel iWorld = this.client.level; + ResourceKey dimensionType = iWorld.dimension(); + if ((shapes.get(dimensionType) == null || shapes.get(dimensionType).isEmpty()) && + (labels.get(dimensionType) == null || labels.get(dimensionType).isEmpty())) + { + return; + } + long currentTime = client.level.getGameTime(); + RenderSystem.enableDepthTest(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + RenderSystem.depthFunc(515); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + // too bright + //RenderSystem.blendFuncSeparate(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE, GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ZERO); + // meh + //RenderSystem.blendFuncSeparate(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SrcFactor.ONE, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA); + + RenderSystem.disableCull(); + RenderSystem.depthMask(false); + //RenderSystem.polygonOffset(-3f, -3f); + //RenderSystem.enablePolygonOffset(); + + Tesselator tesselator = Tesselator.getInstance(); + + // render + double cameraX = camera.getPosition().x; + double cameraY = camera.getPosition().y; + double cameraZ = camera.getPosition().z; + boolean entityBoxes = client.getEntityRenderDispatcher().shouldRenderHitBoxes(); + + if (!shapes.isEmpty()) + { + shapes.get(dimensionType).long2ObjectEntrySet().removeIf( + entry -> entry.getValue().isExpired(currentTime) + ); + Matrix4fStack matrixStack = RenderSystem.getModelViewStack(); + matrixStack.pushMatrix(); + matrixStack.mul(matrices.last().pose()); + RenderSystem.applyModelViewMatrix(); + + // lines + RenderSystem.lineWidth(0.5F); + shapes.get(dimensionType).values().forEach(s -> { + if ((!s.shape.debug || entityBoxes) && s.shouldRender(dimensionType)) + { + s.renderLines(matrices, tesselator, cameraX, cameraY, cameraZ, partialTick); + } + }); + // faces + RenderSystem.lineWidth(0.1F); + shapes.get(dimensionType).values().forEach(s -> { + if ((!s.shape.debug || entityBoxes) && s.shouldRender(dimensionType)) + { + s.renderFaces(tesselator, cameraX, cameraY, cameraZ, partialTick); + } + }); + RenderSystem.lineWidth(1.0F); + matrixStack.popMatrix(); + RenderSystem.applyModelViewMatrix(); + + } + if (!labels.isEmpty()) + { + labels.get(dimensionType).long2ObjectEntrySet().removeIf( + entry -> entry.getValue().isExpired(currentTime) + ); + labels.get(dimensionType).values().forEach(s -> { + if ((!s.shape.debug || entityBoxes) && s.shouldRender(dimensionType)) + { + s.renderLines(matrices, tesselator, cameraX, cameraY, cameraZ, partialTick); + } + }); + } + RenderSystem.enableCull(); + RenderSystem.depthMask(true); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + token.run(); + } + + public void addShapes(ListTag tag) + { + Runnable token = Carpet.startProfilerSection("Scarpet client"); + for (int i = 0, count = tag.size(); i < count; i++) + { + addShape(tag.getCompound(i)); + } + token.run(); + } + + public void addShape(CompoundTag tag) + { + ShapeDispatcher.ExpiringShape shape = ShapeDispatcher.fromTag(tag, client.level); + if (shape == null) + { + return; + } + BiFunction> shapeFactory; + shapeFactory = renderedShapes.get(tag.getString("shape")); + if (shapeFactory == null) + { + CarpetScriptServer.LOG.info("Unrecognized shape: " + tag.getString("shape")); + } + else + { + RenderedShape rshape = shapeFactory.apply(client, shape); + ResourceKey dim = shape.shapeDimension; + long key = rshape.key(); + Map, Long2ObjectOpenHashMap>> container = + rshape.stageDeux() ? labels : shapes; + RenderedShape existing = container.computeIfAbsent(dim, d -> new Long2ObjectOpenHashMap<>()).get(key); + if (existing != null) + { // promoting previous shape + existing.promoteWith(rshape); + } + else + { + container.get(dim).put(key, rshape); + } + + } + } + + public void reset() + { + shapes.values().forEach(Long2ObjectOpenHashMap::clear); + labels.values().forEach(Long2ObjectOpenHashMap::clear); + } + + public void renewShapes() + { + Runnable token = Carpet.startProfilerSection("Scarpet client"); + shapes.values().forEach(el -> el.values().forEach(shape -> shape.expiryTick++)); + labels.values().forEach(el -> el.values().forEach(shape -> shape.expiryTick++)); + + token.run(); + } + + public abstract static class RenderedShape + { + protected T shape; + protected Minecraft client; + long expiryTick; + double renderEpsilon; + + public abstract void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, double cz, float partialTick); + + public void renderFaces(Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + } + + protected RenderedShape(Minecraft client, T shape) + { + this.shape = shape; + this.client = client; + expiryTick = client.level.getGameTime() + shape.getExpiry(); + renderEpsilon = (3 + ((double) key()) / Long.MAX_VALUE) / 1000; + } + + public boolean isExpired(long currentTick) + { + return expiryTick < currentTick; + } + + public long key() + { + return shape.key(client.level.registryAccess()); + } + + public boolean shouldRender(ResourceKey dim) + { + if (shape.followEntity <= 0) + { + return true; + } + if (client.level == null) + { + return false; + } + if (client.level.dimension() != dim) + { + return false; + } + return client.level.getEntity(shape.followEntity) != null; + } + + public boolean stageDeux() + { + return false; + } + + public void promoteWith(RenderedShape rshape) + { + expiryTick = rshape.expiryTick; + } + } + + public static class RenderedSprite extends RenderedShape + { + + private final boolean isitem; + private ItemDisplayContext transformType = ItemDisplayContext.NONE; + + private BlockPos blockPos; + private BlockState blockState; + private BlockEntity BlockEntity = null; + + protected RenderedSprite(Minecraft client, ShapeDispatcher.ExpiringShape shape, boolean isitem) + { + super(client, (ShapeDispatcher.DisplayedSprite) shape); + this.isitem = isitem; + if (isitem) + { + this.transformType = ItemDisplayContext.valueOf(((ShapeDispatcher.DisplayedSprite) shape).itemTransformType.toUpperCase(Locale.ROOT)); + } + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, + double cz, float partialTick) + { + if (shape.a == 0.0) + { + return; + } + + Vec3 v1 = shape.relativiseRender(client.level, shape.pos, partialTick); + Camera camera1 = client.gameRenderer.getMainCamera(); + + matrices.pushPose(); + if (!isitem)// blocks should use its center as the origin + { + matrices.translate(0.5, 0.5, 0.5); + } + + matrices.translate(v1.x - cx, v1.y - cy, v1.z - cz); + rotatePoseStackByShapeDirection(matrices, shape.facing, camera1, isitem ? v1 : v1.add(0.5, 0.5, 0.5)); + if (shape.tilt != 0.0f) + { + matrices.mulPose(Axis.ZP.rotationDegrees(-shape.tilt)); + } + if (shape.lean != 0.0f) + { + matrices.mulPose(Axis.XP.rotationDegrees(-shape.lean)); + } + if (shape.turn != 0.0f) + { + matrices.mulPose(Axis.YP.rotationDegrees(shape.turn)); + } + matrices.scale(shape.scaleX, shape.scaleY, shape.scaleZ); + + if (!isitem) + { + // blocks should use its center as the origin + matrices.translate(-0.5, -0.5, -0.5); + } + else + { + // items seems to be flipped by default + matrices.mulPose(Axis.YP.rotationDegrees(180)); + } + + RenderSystem.depthMask(true); + RenderSystem.enableCull(); + RenderSystem.enableDepthTest(); + + blockPos = BlockPos.containing(v1); + int light = 0; + if (client.level != null) + { + light = LightTexture.pack( + shape.blockLight < 0 ? client.level.getBrightness(LightLayer.BLOCK, blockPos) : shape.blockLight, + shape.skyLight < 0 ? client.level.getBrightness(LightLayer.SKY, blockPos) : shape.skyLight + ); + } + + blockState = shape.blockState; + + MultiBufferSource.BufferSource immediate = client.renderBuffers().bufferSource(); + if (!isitem) + { + // draw the block itself + if (blockState.getRenderShape() == RenderShape.MODEL) + { + + var bakedModel = client.getBlockRenderer().getBlockModel(blockState); + int color = client.getBlockColors().getColor(blockState, client.level, blockPos, 0); + //dont know why there is a 0. + //see https://github.com/senseiwells/EssentialClient/blob/4db1f291936f502304791ee323f369c206b3021d/src/main/java/me/senseiwells/essentialclient/utils/render/RenderHelper.java#L464 + float red = (color >> 16 & 0xFF) / 255.0F; + float green = (color >> 8 & 0xFF) / 255.0F; + float blue = (color & 0xFF) / 255.0F; + RenderType type; + if (blockState.getBlock() instanceof LeavesBlock && !Minecraft.useFancyGraphics()) { + type = RenderType.solid(); + } else { + type = ItemBlockRenderTypes.getRenderType(blockState, false); + } + client.getBlockRenderer().getModelRenderer().renderModel(matrices.last(), immediate.getBuffer(type), blockState, bakedModel, red, green, blue, light, OverlayTexture.NO_OVERLAY); + } + + // draw the block`s entity part + if (BlockEntity == null) + { + if (blockState.getBlock() instanceof EntityBlock eb) + { + BlockEntity = eb.newBlockEntity(blockPos, blockState); + if (BlockEntity != null) + { + BlockEntity.setLevel(client.level); + if (shape.blockEntity != null) + { + BlockEntity.loadWithComponents(shape.blockEntity, client.level.registryAccess()); + } + } + } + } + if (BlockEntity instanceof ShulkerBoxBlockEntity sbBlockEntity) + { + sbrender(sbBlockEntity, partialTick, + matrices, immediate, light, OverlayTexture.NO_OVERLAY); + } + else + { + if (BlockEntity != null) + { + BlockEntityRenderer blockEntityRenderer = client.getBlockEntityRenderDispatcher().getRenderer(BlockEntity); + if (blockEntityRenderer != null) + { + blockEntityRenderer.render(BlockEntity, partialTick, + matrices, immediate, light, OverlayTexture.NO_OVERLAY); + + } + } + } + } + else + { + if (shape.item != null) + { + // draw the item + client.getItemRenderer().renderStatic(shape.item, transformType, light, + OverlayTexture.NO_OVERLAY, matrices, immediate, client.level, (int) shape.key(client.level.registryAccess())); + } + } + matrices.popPose(); + immediate.endBatch(); + RenderSystem.disableCull(); + RenderSystem.disableDepthTest(); + RenderSystem.depthMask(false); + + } + + @Override + public boolean stageDeux() + { + return true; + } + + // copy and modifiy a bit from net.minecraft.client.renderer.blockentity.ShulkerBoxRenderer.render + public void sbrender(ShulkerBoxBlockEntity shulkerBoxBlockEntity, float f, PoseStack poseStack, MultiBufferSource multiBufferSource, int i, int j) + { + Direction direction = Direction.UP; + if (shulkerBoxBlockEntity.hasLevel()) + { + BlockState blockState = shulkerBoxBlockEntity.getBlockState(); + if (blockState.getBlock() instanceof ShulkerBoxBlock) + { + direction = blockState.getValue(ShulkerBoxBlock.FACING); + } + } + DyeColor dyeColor = shulkerBoxBlockEntity.getColor(); + Material material; + if (dyeColor == null) + { + material = Sheets.DEFAULT_SHULKER_TEXTURE_LOCATION; + } + else + { + material = Sheets.SHULKER_TEXTURE_LOCATION.get(dyeColor.getId()); + } + + poseStack.pushPose(); + poseStack.translate(0.5, 0.5, 0.5); + poseStack.scale(0.9995F, 0.9995F, 0.9995F); + poseStack.mulPose(direction.getRotation()); + poseStack.scale(1.0F, -1.0F, -1.0F); + poseStack.translate(0.0, -1.0, 0.0); + ShulkerModel model = VanillaClient.ShulkerBoxRenderer_model(client.getBlockEntityRenderDispatcher().getRenderer(shulkerBoxBlockEntity)); + ModelPart modelPart = model.getLid(); + modelPart.setPos(0.0F, 24.0F - shulkerBoxBlockEntity.getProgress(f) * 0.5F * 16.0F, 0.0F); + modelPart.yRot = 270.0F * shulkerBoxBlockEntity.getProgress(f) * (float) (Math.PI / 180.0); + VertexConsumer vertexConsumer = material.buffer(multiBufferSource, RenderType::entityCutoutNoCull); + model.renderToBuffer(poseStack, vertexConsumer, i, j, 0xffffffff); + poseStack.popPose(); + } + } + + + public static class RenderedText extends RenderedShape + { + + protected RenderedText(Minecraft client, ShapeDispatcher.ExpiringShape shape) + { + super(client, (ShapeDispatcher.DisplayedText) shape); + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.a == 0.0) + { + return; + } + Vec3 v1 = shape.relativiseRender(client.level, shape.pos, partialTick); + Camera camera1 = client.gameRenderer.getMainCamera(); + Font textRenderer = client.font; + if (shape.doublesided) + { + RenderSystem.disableCull(); + } + else + { + RenderSystem.enableCull(); + } + matrices.pushPose(); + matrices.translate(v1.x - cx, v1.y - cy, v1.z - cz); + + rotatePoseStackByShapeDirection(matrices, shape.facing, camera1, v1); + + matrices.scale(shape.size * 0.0025f, -shape.size * 0.0025f, shape.size * 0.0025f); + //RenderSystem.scalef(shape.size* 0.0025f, -shape.size*0.0025f, shape.size*0.0025f); + if (shape.tilt != 0.0f) + { + matrices.mulPose(Axis.ZP.rotationDegrees(shape.tilt)); + } + if (shape.lean != 0.0f) + { + matrices.mulPose(Axis.XP.rotationDegrees(shape.lean)); + } + if (shape.turn != 0.0f) + { + matrices.mulPose(Axis.YP.rotationDegrees(shape.turn)); + } + matrices.translate(-10 * shape.indent, -10 * shape.height - 9, (-10 * renderEpsilon) - 10 * shape.raise); + //if (visibleThroughWalls) RenderSystem.disableDepthTest(); + matrices.scale(-1, 1, 1); + //RenderSystem.applyModelViewMatrix(); // passed matrix directly to textRenderer.draw, not AffineTransformation.identity().getMatrix(), + + float text_x = 0; + if (shape.align == 0) + { + text_x = (float) (-textRenderer.width(shape.value.getString())) / 2.0F; + } + else if (shape.align == 1) + { + text_x = (float) (-textRenderer.width(shape.value.getString())); + } + MultiBufferSource.BufferSource immediate = MultiBufferSource.immediate(new ByteBufferBuilder(RenderType.TRANSIENT_BUFFER_SIZE)); + textRenderer.drawInBatch(shape.value, text_x, 0.0F, shape.textcolor, false, matrices.last().pose(), immediate, Font.DisplayMode.NORMAL, shape.textbck, 15728880); + immediate.endBatch(); + matrices.popPose(); + RenderSystem.enableCull(); + } + + @Override + public boolean stageDeux() + { + return true; + } + + @Override + public void promoteWith(RenderedShape rshape) + { + super.promoteWith(rshape); + try + { + this.shape.value = ((ShapeDispatcher.DisplayedText) rshape.shape).value; + } + catch (ClassCastException ignored) + { + CarpetScriptServer.LOG.error("shape " + rshape.shape.getClass() + " cannot cast to a Label"); + } + } + } + + public static class RenderedBox extends RenderedShape + { + + private RenderedBox(Minecraft client, ShapeDispatcher.ExpiringShape shape) + { + super(client, (ShapeDispatcher.Box) shape); + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.a == 0.0) + { + return; + } + Vec3 v1 = shape.relativiseRender(client.level, shape.from, partialTick); + Vec3 v2 = shape.relativiseRender(client.level, shape.to, partialTick); + drawBoxWireGLLines(tesselator, + (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(Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.fa == 0.0) + { + return; + } + Vec3 v1 = shape.relativiseRender(client.level, shape.from, partialTick); + Vec3 v2 = shape.relativiseRender(client.level, shape.to, partialTick); + // consider using built-ins + //DebugRenderer.drawBox(new Box(v1.x, v1.y, v1.z, v2.x, v2.y, v2.z), 0.5f, 0.5f, 0.5f, 0.5f);//shape.r, shape.g, shape.b, shape.a); + drawBoxFaces(tesselator, + (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(Minecraft client, ShapeDispatcher.ExpiringShape shape) + { + super(client, (ShapeDispatcher.Line) shape); + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + Vec3 v1 = shape.relativiseRender(client.level, shape.from, partialTick); + Vec3 v2 = shape.relativiseRender(client.level, shape.to, partialTick); + drawLine(tesselator, + (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 RenderedPolyface extends RenderedShape + { + private static final VertexFormat.Mode[] faceIndices = new VertexFormat.Mode[]{ + Mode.LINES, Mode.LINE_STRIP, Mode.DEBUG_LINES, Mode.DEBUG_LINE_STRIP, Mode.TRIANGLES, Mode.TRIANGLE_STRIP, Mode.TRIANGLE_FAN, Mode.QUADS}; + + public RenderedPolyface(Minecraft client, ShapeDispatcher.ExpiringShape shape) + { + super(client, (ShapeDispatcher.Polyface) shape); + } + + @Override + public void renderFaces(Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.fa == 0) + { + return; + } + + if (shape.doublesided) + { + RenderSystem.disableCull(); + } + else + { + RenderSystem.enableCull(); + } + + BufferBuilder builder = tesselator.begin(faceIndices[shape.mode], DefaultVertexFormat.POSITION_COLOR); + for (int i = 0; i < shape.vertexList.size(); i++) + { + Vec3 vec = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + builder.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.fr, shape.fg, shape.fb, shape.fa); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + + RenderSystem.disableCull(); + RenderSystem.depthMask(false); + //RenderSystem.enableDepthTest(); + + + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, + double cz, float partialTick) + { + if (shape.a == 0) + { + return; + } + + if (shape.mode == 6) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); + Vec3 vec0 = null; + for (int i = 0; i < shape.vertexList.size(); i++) + { + Vec3 vec = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + if (i == 0) + { + vec0 = vec; + } + builder.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + } + builder.addVertex((float) (vec0.x() - cx), (float) (vec0.y() - cy), (float) (vec0.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + BufferUploader.drawWithShader(builder.buildOrThrow()); + if (shape.inneredges) + { + BufferBuilder builderr = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + for (int i = 1; i < shape.vertexList.size() - 1; i++) + { + Vec3 vec = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + + builderr.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + builderr.addVertex((float) (vec0.x() - cx), (float) (vec0.y() - cy), (float) (vec0.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + } + BufferUploader.drawWithShader(builderr.buildOrThrow()); + } + return; + } + if (shape.mode == 5) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); + Vec3 vec = shape.vertexList.get(1); + if (shape.relative.get(1)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + builder.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + int i; + for (i = 0; i < shape.vertexList.size(); i += 2) + { + vec = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + builder.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + } + i = shape.vertexList.size() - 1; + for (i -= 1 - i % 2; i > 0; i -= 2) + { + vec = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + builder.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + } + if (shape.inneredges) + { + for (i = 2; i < shape.vertexList.size() - 1; i++) + { + vec = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vec = shape.relativiseRender(client.level, vec, partialTick); + } + builder.addVertex((float) (vec.x() - cx), (float) (vec.y() - cy), (float) (vec.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + } + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + return; + } + if (shape.mode == 4) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + for (int i = 0; i < shape.vertexList.size(); i++) + { + Vec3 vecA = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vecA = shape.relativiseRender(client.level, vecA, partialTick); + } + i++; + Vec3 vecB = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vecB = shape.relativiseRender(client.level, vecB, partialTick); + } + i++; + Vec3 vecC = shape.vertexList.get(i); + if (shape.relative.get(i)) + { + vecC = shape.relativiseRender(client.level, vecC, partialTick); + } + builder.addVertex((float) (vecA.x() - cx), (float) (vecA.y() - cy), (float) (vecA.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + builder.addVertex((float) (vecB.x() - cx), (float) (vecB.y() - cy), (float) (vecB.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + + builder.addVertex((float) (vecB.x() - cx), (float) (vecB.y() - cy), (float) (vecB.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + builder.addVertex((float) (vecC.x() - cx), (float) (vecC.y() - cy), (float) (vecC.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + + builder.addVertex((float) (vecC.x() - cx), (float) (vecC.y() - cy), (float) (vecC.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + builder.addVertex((float) (vecA.x() - cx), (float) (vecA.y() - cy), (float) (vecA.z() - cz)).setColor(shape.r, shape.g, shape.b, shape.a); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + } + } + + public static class RenderedSphere extends RenderedShape + { + public RenderedSphere(Minecraft client, ShapeDispatcher.ExpiringShape shape) + { + super(client, (ShapeDispatcher.Sphere) shape); + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.a == 0.0) + { + return; + } + Vec3 vc = shape.relativiseRender(client.level, shape.center, partialTick); + drawSphereWireframe(tesselator, + (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(Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.fa == 0.0) + { + return; + } + Vec3 vc = shape.relativiseRender(client.level, shape.center, partialTick); + drawSphereFaces(tesselator, + (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(Minecraft client, ShapeDispatcher.ExpiringShape shape) + { + super(client, (ShapeDispatcher.Cylinder) shape); + } + + @Override + public void renderLines(PoseStack matrices, Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.a == 0.0) + { + return; + } + Vec3 vc = shape.relativiseRender(client.level, shape.center, partialTick); + double dir = Mth.sign(shape.height); + drawCylinderWireframe(tesselator, + (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(Tesselator tesselator, double cx, double cy, double cz, float partialTick) + { + if (shape.fa == 0.0) + { + return; + } + Vec3 vc = shape.relativiseRender(client.level, shape.center, partialTick); + double dir = Mth.sign(shape.height); + drawCylinderFaces(tesselator, + (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(Tesselator tesselator, float x1, float y1, float z1, float x2, float y2, float z2, float red1, float grn1, float blu1, float alpha) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + builder.addVertex(x1, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + public static void drawBoxWireGLLines( + Tesselator tesselator, + 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) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + if (xthick) + { + builder.addVertex(x1, y1, z1).setColor(red1, grn2, blu2, alpha); + builder.addVertex(x2, y1, z1).setColor(red1, grn2, blu2, alpha); + + builder.addVertex(x2, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z1).setColor(red1, grn1, blu1, alpha); + + builder.addVertex(x1, y1, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z2).setColor(red1, grn1, blu1, alpha); + + builder.addVertex(x1, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + } + if (ythick) + { + builder.addVertex(x1, y1, z1).setColor(red2, grn1, blu2, alpha); + builder.addVertex(x1, y2, z1).setColor(red2, grn1, blu2, alpha); + + builder.addVertex(x2, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z1).setColor(red1, grn1, blu1, alpha); + + builder.addVertex(x1, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y1, z2).setColor(red1, grn1, blu1, alpha); + + builder.addVertex(x2, y1, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + } + if (zthick) + { + builder.addVertex(x1, y1, z1).setColor(red2, grn2, blu1, alpha); + builder.addVertex(x1, y1, z2).setColor(red2, grn2, blu1, alpha); + + builder.addVertex(x1, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z2).setColor(red1, grn1, blu1, alpha); + + builder.addVertex(x2, y1, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z1).setColor(red1, grn1, blu1, alpha); + + builder.addVertex(x2, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + public static void drawBoxFaces( + Tesselator tesselator, + 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) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + + if (xthick && ythick) + { + builder.addVertex(x1, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z1).setColor(red1, grn1, blu1, alpha); + if (zthick) + { + builder.addVertex(x1, y1, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z2).setColor(red1, grn1, blu1, alpha); + } + } + + + if (zthick && ythick) + { + builder.addVertex(x1, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y1, z2).setColor(red1, grn1, blu1, alpha); + + if (xthick) + { + builder.addVertex(x2, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z1).setColor(red1, grn1, blu1, alpha); + } + } + + // now at least drawing one + if (zthick && xthick) + { + builder.addVertex(x1, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y1, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y1, z2).setColor(red1, grn1, blu1, alpha); + + + if (ythick) + { + builder.addVertex(x1, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z1).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x2, y2, z2).setColor(red1, grn1, blu1, alpha); + builder.addVertex(x1, y2, z2).setColor(red1, grn1, blu1, alpha); + } + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + public static void drawCylinderWireframe(Tesselator tesselator, + 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(Mth.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; + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + for (int i = 0; i <= num_steps360 + 1; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float y = hh; + float z = r * Mth.sin(theta); + builder.addVertex(x + cx, y + cy, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + if (!isFlat) + { + for (int i = 0; i <= num_steps180; i++) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + float theta = step * i; + float x = r * Mth.cos(theta); + + float z = r * Mth.sin(theta); + + builder.addVertex(cx - x, cy + 0, cz + z).setColor(red, grn, blu, alpha); + builder.addVertex(cx + x, cy + 0, cz - z).setColor(red, grn, blu, alpha); + builder.addVertex(cx + x, cy + h, cz - z).setColor(red, grn, blu, alpha); + builder.addVertex(cx - x, cy + h, cz + z).setColor(red, grn, blu, alpha); + builder.addVertex(cx - x, cy + 0, cz + z).setColor(red, grn, blu, alpha); + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + } + else + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + for (int i = 0; i <= num_steps180; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builder.addVertex(cx - x, cy, cz + z).setColor(red, grn, blu, alpha); + builder.addVertex(cx + x, cy, cz - z).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + } + else if (axis == Direction.Axis.X) + { + for (int dh = 0; dh < hsteps; dh++) + { + float hh = dh * hstep; + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float z = r * Mth.cos(theta); + float x = hh; + float y = r * Mth.sin(theta); + builder.addVertex(x + cx, y + cy, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + if (!isFlat) + { + for (int i = 0; i <= num_steps180; i++) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + float theta = step * i; + float y = r * Mth.cos(theta); + + float z = r * Mth.sin(theta); + + builder.addVertex(cx + 0, cy - y, cz + z).setColor(red, grn, blu, alpha); + builder.addVertex(cx + 0, cy + y, cz - z).setColor(red, grn, blu, alpha); + builder.addVertex(cx + h, cy + y, cz - z).setColor(red, grn, blu, alpha); + builder.addVertex(cx + h, cy - y, cz + z).setColor(red, grn, blu, alpha); + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + } + else + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + for (int i = 0; i <= num_steps180; i++) + { + float theta = step * i; + float y = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builder.addVertex(cx, cy - y, cz + z).setColor(red, grn, blu, alpha); + builder.addVertex(cx, cy + y, cz - z).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + } + else if (axis == Direction.Axis.Z) + { + for (int dh = 0; dh < hsteps; dh++) + { + float hh = dh * hstep; + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float y = r * Mth.cos(theta); + float z = hh; + float x = r * Mth.sin(theta); + builder.addVertex(x + cx, y + cy, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + if (!isFlat) + { + for (int i = 0; i <= num_steps180; i++) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + float theta = step * i; + float x = r * Mth.cos(theta); + + float y = r * Mth.sin(theta); + + builder.addVertex(cx + x, cy - y, cz + 0).setColor(red, grn, blu, alpha); + builder.addVertex(cx - x, cy + y, cz + 0).setColor(red, grn, blu, alpha); + builder.addVertex(cx - x, cy + y, cz + h).setColor(red, grn, blu, alpha); + builder.addVertex(cx + x, cy - y, cz + h).setColor(red, grn, blu, alpha); + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + } + else + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINES, DefaultVertexFormat.POSITION_COLOR); + for (int i = 0; i <= num_steps180; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float y = r * Mth.sin(theta); + builder.addVertex(cx + x, cy - y, cz).setColor(red, grn, blu, alpha); + builder.addVertex(cx - x, cy + y, cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + } + } + + public static void drawCylinderFaces(Tesselator tesselator, + 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); + //final int num_steps180 = (int) (Math.PI / step) + 1; + int num_steps360 = (int) (2 * Math.PI / step) + 1; + + if (axis == Direction.Axis.Y) + { + + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION_COLOR); + builder.addVertex(cx, cy, cz).setColor(red, grn, blu, alpha); + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builder.addVertex(x + cx, cy, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + if (!isFlat) + { + BufferBuilder builderr = tesselator.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION_COLOR); + builderr.addVertex(cx, cy + h, cz).setColor(red, grn, blu, alpha); + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builderr.addVertex(x + cx, cy + h, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builderr.buildOrThrow()); + + BufferBuilder builderrr = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); // quad strip to quads + float xp = r * 1; + float zp = r * 0; + for (int i = 1; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builderrr.addVertex(cx + xp, cy + 0, cz + zp).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + xp, cy + h, cz + zp).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + x, cy + h, cz + z).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + x, cy + 0, cz + z).setColor(red, grn, blu, alpha); + xp = x; + zp = z; + } + BufferUploader.drawWithShader(builderrr.buildOrThrow()); + } + + } + else if (axis == Direction.Axis.X) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION_COLOR); + builder.addVertex(cx, cy, cz).setColor(red, grn, blu, alpha); + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float y = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builder.addVertex(cx, cy + y, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + if (!isFlat) + { + BufferBuilder builderr = tesselator.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION_COLOR); + builderr.addVertex(cx + h, cy, cz).setColor(red, grn, blu, alpha); + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float y = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builderr.addVertex(cx + h, cy + y, cz + z).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builderr.buildOrThrow()); + + BufferBuilder builderrr = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); // quad strip to quads + float yp = r * 1; + float zp = r * 0; + for (int i = 1; i <= num_steps360; i++) + { + float theta = step * i; + float y = r * Mth.cos(theta); + float z = r * Mth.sin(theta); + builderrr.addVertex(cx + 0, cy + yp, cz + zp).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + h, cy + yp, cz + zp).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + h, cy + y, cz + z).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + 0, cy + y, cz + z).setColor(red, grn, blu, alpha); + yp = y; + zp = z; + } + BufferUploader.drawWithShader(builderrr.buildOrThrow()); + } + } + else if (axis == Direction.Axis.Z) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION_COLOR); + builder.addVertex(cx, cy, cz).setColor(red, grn, blu, alpha); + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float y = r * Mth.sin(theta); + builder.addVertex(x + cx, cy + y, cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + if (!isFlat) + { + BufferBuilder builderr = tesselator.begin(VertexFormat.Mode.TRIANGLE_FAN, DefaultVertexFormat.POSITION_COLOR); + builderr.addVertex(cx, cy, cz + h).setColor(red, grn, blu, alpha); + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float y = r * Mth.sin(theta); + builderr.addVertex(x + cx, cy + y, cz + h).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builderr.buildOrThrow()); + + BufferBuilder builderrr = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); // quad strip to quads + float xp = r; + float yp = 0; + for (int i = 1; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.cos(theta); + float y = r * Mth.sin(theta); + builderrr.addVertex(cx + xp, cy + yp, cz + 0).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + xp, cy + yp, cz + h).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + x, cy + y, cz + h).setColor(red, grn, blu, alpha); + builderrr.addVertex(cx + x, cy + y, cz + 0).setColor(red, grn, blu, alpha); + xp = x; + yp = y; + } + BufferUploader.drawWithShader(builderrr.buildOrThrow()); + } + } + } + + public static void drawSphereWireframe(Tesselator tesselator, + 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) + 1; + for (int i = 0; i <= num_steps360; i++) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); + float theta = step * i; + for (int j = 0; j <= num_steps180; j++) + { + float phi = step * j; + float x = r * Mth.sin(phi) * Mth.cos(theta); + float z = r * Mth.sin(phi) * Mth.sin(theta); + float y = r * Mth.cos(phi); + builder.addVertex(x + cx, y + cy, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + for (int j = 0; j <= num_steps180; j++) + { + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.DEBUG_LINE_STRIP, DefaultVertexFormat.POSITION_COLOR); // line loop to line strip + float phi = step * j; + + for (int i = 0; i <= num_steps360; i++) + { + float theta = step * i; + float x = r * Mth.sin(phi) * Mth.cos(theta); + float z = r * Mth.sin(phi) * Mth.sin(theta); + float y = r * Mth.cos(phi); + builder.addVertex(x + cx, y + cy, z + cz).setColor(red, grn, blu, alpha); + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + + } + + public static void drawSphereFaces(Tesselator tesselator, + 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; + BufferBuilder builder = tesselator.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); // quad strip to quads + float xb = 0; + float zb = 0; + float xbp = 0; + float zbp = 0; + float yp = r; + for (int j = 0; j <= num_steps180; j++) + { + float phi = j * step; + float x = r * Mth.sin(phi) * Mth.cos(theta); + float z = r * Mth.sin(phi) * Mth.sin(theta); + float y = r * Mth.cos(phi); + float xp = r * Mth.sin(phi) * Mth.cos(thetaprime); + float zp = r * Mth.sin(phi) * Mth.sin(thetaprime); + builder.addVertex(xb + cx, yp + cy, zb + cz).setColor(red, grn, blu, alpha); + builder.addVertex(xbp + cx, yp + cy, zbp + cz).setColor(red, grn, blu, alpha); + builder.addVertex(xp + cx, y + cy, zp + cz).setColor(red, grn, blu, alpha); + builder.addVertex(x + cx, y + cy, z + cz).setColor(red, grn, blu, alpha); + xb = x; + zb = z; + xbp = xp; + zbp = zp; + yp = y; + } + BufferUploader.drawWithShader(builder.buildOrThrow()); + } + } +} diff --git a/src/main/java/carpet/script/utils/SimplexNoiseSampler.java b/src/main/java/carpet/script/utils/SimplexNoiseSampler.java new file mode 100644 index 0000000..0237e5a --- /dev/null +++ b/src/main/java/carpet/script/utils/SimplexNoiseSampler.java @@ -0,0 +1,203 @@ +package carpet.script.utils; + +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.util.Mth; + +import java.util.Map; +import java.util.Random; + +// extracted from import net.minecraft.util.math.noise.SimplexNoiseSampler +public class SimplexNoiseSampler extends PerlinNoiseSampler +{ + private static final double sqrt3 = Math.sqrt(3.0D); + private static final double SKEW_FACTOR_2D; + private static final double UNSKEW_FACTOR_2D; + + public static SimplexNoiseSampler instance = new SimplexNoiseSampler(new Random(0)); + public static Map samplers = new Long2ObjectOpenHashMap<>(); + + public static SimplexNoiseSampler getSimplex(long aLong) + { + if (samplers.size() > 256) + { + samplers.clear(); + } + return samplers.computeIfAbsent(aLong, seed -> new SimplexNoiseSampler(new Random(seed))); + } + + public SimplexNoiseSampler(Random random) + { + super(random); + } + + private double grad(int hash, double x, double y, double z, double d) + { + double e = d - x * x - y * y - z * z; + double g; + if (e < 0.0D) + { + g = 0.0D; + } + else + { + e *= e; + g = e * e * PerlinNoiseSampler.dot3d(PerlinNoiseSampler.gradients3d[hash], x, y, z); + } + + return g; + } + + @Override + public double sample2d(double x, double y) + { + x = x / 2; + y = y / 2; + double d = (x + y) * SKEW_FACTOR_2D; + int i = Mth.floor(x + d); + int j = Mth.floor(y + d); + double e = (i + j) * UNSKEW_FACTOR_2D; + double f = i - e; + double g = j - e; + double h = x - f; + double k = y - g; + byte n; + byte o; + if (h > k) + { + n = 1; + o = 0; + } + else + { + n = 0; + o = 1; + } + + double p = h - n + UNSKEW_FACTOR_2D; + double q = k - o + UNSKEW_FACTOR_2D; + double r = h - 1.0D + 2.0D * UNSKEW_FACTOR_2D; + double s = k - 1.0D + 2.0D * UNSKEW_FACTOR_2D; + int t = i & 255; + int u = j & 255; + int v = this.getGradient(t + this.getGradient(u)) % 12; + int w = this.getGradient(t + n + this.getGradient(u + o)) % 12; + int z = this.getGradient(t + 1 + this.getGradient(u + 1)) % 12; + double aa = this.grad(v, h, k, 0.0D, 0.5D); + double ab = this.grad(w, p, q, 0.0D, 0.5D); + double ac = this.grad(z, r, s, 0.0D, 0.5D); + //return 70.0D * (aa + ab + ac); + return 35.0D * (aa + ab + ac) + 0.5; + } + + @Override + public double sample3d(double d, double e, double f) + { + d = d / 2; + e = e / 2; + f = f / 2; + //final double g = 0.3333333333333333D; + double h = (d + e + f) * 0.3333333333333333D; + int i = Mth.floor(d + h); + int j = Mth.floor(e + h); + int k = Mth.floor(f + h); + //final double l = 0.16666666666666666D; + double m = (i + j + k) * 0.16666666666666666D; + double n = i - m; + double o = j - m; + double p = k - m; + double q = d - n; + double r = e - o; + double s = f - p; + byte z; + byte aa; + byte ab; + byte ac; + byte ad; + byte bc; + if (q >= r) + { + if (r >= s) + { + z = 1; + aa = 0; + ab = 0; + ac = 1; + ad = 1; + bc = 0; + } + else if (q >= s) + { + z = 1; + aa = 0; + ab = 0; + ac = 1; + ad = 0; + bc = 1; + } + else + { + z = 0; + aa = 0; + ab = 1; + ac = 1; + ad = 0; + bc = 1; + } + } + else if (r < s) + { + z = 0; + aa = 0; + ab = 1; + ac = 0; + ad = 1; + bc = 1; + } + else if (q < s) + { + z = 0; + aa = 1; + ab = 0; + ac = 0; + ad = 1; + bc = 1; + } + else + { + z = 0; + aa = 1; + ab = 0; + ac = 1; + ad = 1; + bc = 0; + } + + double bd = q - z + 0.16666666666666666D; + double be = r - aa + 0.16666666666666666D; + double bf = s - ab + 0.16666666666666666D; + double bg = q - ac + 0.3333333333333333D; + double bh = r - ad + 0.3333333333333333D; + double bi = s - bc + 0.3333333333333333D; + double bj = q - 1.0D + 0.5D; + double bk = r - 1.0D + 0.5D; + double bl = s - 1.0D + 0.5D; + int bm = i & 255; + int bn = j & 255; + int bo = k & 255; + int bp = this.getGradient(bm + this.getGradient(bn + this.getGradient(bo))) % 12; + int bq = this.getGradient(bm + z + this.getGradient(bn + aa + this.getGradient(bo + ab))) % 12; + int br = this.getGradient(bm + ac + this.getGradient(bn + ad + this.getGradient(bo + bc))) % 12; + int bs = this.getGradient(bm + 1 + this.getGradient(bn + 1 + this.getGradient(bo + 1))) % 12; + double bt = this.grad(bp, q, r, s, 0.6D); + double bu = this.grad(bq, bd, be, bf, 0.6D); + double bv = this.grad(br, bg, bh, bi, 0.6D); + double bw = this.grad(bs, bj, bk, bl, 0.6D); + return 16.0D * (bt + bu + bv + bw) + 0.5; + } + + static + { + SKEW_FACTOR_2D = 0.5D * (sqrt3 - 1.0D); + UNSKEW_FACTOR_2D = (3.0D - sqrt3) / 6.0D; + } +} \ No newline at end of file diff --git a/src/main/java/carpet/script/utils/SnoopyCommandSource.java b/src/main/java/carpet/script/utils/SnoopyCommandSource.java new file mode 100644 index 0000000..39d582e --- /dev/null +++ b/src/main/java/carpet/script/utils/SnoopyCommandSource.java @@ -0,0 +1,207 @@ +package carpet.script.utils; + +import carpet.script.external.Vanilla; +import net.minecraft.commands.CommandResultCallback; +import net.minecraft.commands.CommandSigningContext; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.TaskChainer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; + +public class SnoopyCommandSource extends CommandSourceStack +{ + private final CommandSource output; + private final Vec3 position; + private final ServerLevel world; + private final int level; + private final String simpleName; + private final Component name; + private final MinecraftServer server; + // skipping silent since snooper is never silent + private final Entity entity; + private final CommandResultCallback resultConsumer; + private final EntityAnchorArgument.Anchor entityAnchor; + private final Vec2 rotation; + // good stuff + private final Component[] error; + private final List chatOutput; + private final CommandSigningContext signingContext; + + private final TaskChainer taskChainer; + + public SnoopyCommandSource(CommandSourceStack original, Component[] error, List chatOutput) + { + super(CommandSource.NULL, original.getPosition(), original.getRotation(), original.getLevel(), Vanilla.MinecraftServer_getRunPermissionLevel(original.getServer()), + original.getTextName(), original.getDisplayName(), original.getServer(), original.getEntity(), false, + CommandResultCallback.EMPTY, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(original.getServer())); + this.output = CommandSource.NULL; + this.position = original.getPosition(); + this.world = original.getLevel(); + this.level = Vanilla.MinecraftServer_getRunPermissionLevel(original.getServer()); + this.simpleName = original.getTextName(); + this.name = original.getDisplayName(); + this.server = original.getServer(); + this.entity = original.getEntity(); + this.resultConsumer = CommandResultCallback.EMPTY; + this.entityAnchor = original.getAnchor(); + this.rotation = original.getRotation(); + this.error = error; + this.chatOutput = chatOutput; + this.signingContext = original.getSigningContext(); + this.taskChainer = TaskChainer.immediate(original.getServer()); + } + + public SnoopyCommandSource(ServerPlayer player, Component[] error, List output) + { + super(player, player.position(), player.getRotationVector(), + player.level() instanceof final ServerLevel serverLevel ? serverLevel : null, + player.server.getProfilePermissions(player.getGameProfile()), player.getName().getString(), player.getDisplayName(), + player.level().getServer(), player); + this.output = player; + this.position = player.position(); + this.world = player.level() instanceof final ServerLevel serverLevel ? serverLevel : null; + this.level = player.server.getProfilePermissions(player.getGameProfile()); + this.simpleName = player.getName().getString(); + this.name = player.getDisplayName(); + this.server = player.level().getServer(); + this.entity = player; + this.resultConsumer = CommandResultCallback.EMPTY; + this.entityAnchor = EntityAnchorArgument.Anchor.FEET; + this.rotation = player.getRotationVector(); // not a client call really + this.error = error; + this.chatOutput = output; + this.signingContext = CommandSigningContext.ANONYMOUS; + this.taskChainer = TaskChainer.immediate(player.server); + } + + private SnoopyCommandSource(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String simpleName, Component name, MinecraftServer server, @Nullable Entity entity, CommandResultCallback consumer, EntityAnchorArgument.Anchor entityAnchor, CommandSigningContext context, TaskChainer chainer, + Component[] error, List chatOutput + ) + { + super(output, pos, rot, world, level, + simpleName, name, server, entity, false, + consumer, entityAnchor, context, chainer); + this.output = output; + this.position = pos; + this.rotation = rot; + this.world = world; + this.level = level; + this.simpleName = simpleName; + this.name = name; + this.server = server; + this.entity = entity; + this.resultConsumer = consumer; + this.entityAnchor = entityAnchor; + this.error = error; + this.chatOutput = chatOutput; + this.signingContext = context; + this.taskChainer = chainer; + } + + @Override + public CommandSourceStack withEntity(Entity entity) + { + return new SnoopyCommandSource(output, position, rotation, world, level, entity.getName().getString(), entity.getDisplayName(), server, entity, resultConsumer, entityAnchor, signingContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack withPosition(Vec3 position) + { + return new SnoopyCommandSource(output, position, rotation, world, level, simpleName, name, server, entity, resultConsumer, entityAnchor, signingContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack withRotation(Vec2 rotation) + { + return new SnoopyCommandSource(output, position, rotation, world, level, simpleName, name, server, entity, resultConsumer, entityAnchor, signingContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack withCallback(CommandResultCallback consumer) + { + return new SnoopyCommandSource(output, position, rotation, world, level, simpleName, name, server, entity, consumer, entityAnchor, signingContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack withCallback(CommandResultCallback consumer, BinaryOperator binaryOperator) + { + CommandResultCallback resultConsumer = binaryOperator.apply(this.resultConsumer, consumer); + return this.withCallback(resultConsumer); + } + + //@Override // only used in fuctions and we really don't care to track these actually, besides the basic output + // also other overrides target ONLY execute command, which withSilent doesn't care bout. + //public ServerCommandSource withSilent() { return this; } + + @Override + public CommandSourceStack withPermission(int level) + { + return this; + } + + @Override + public CommandSourceStack withMaximumPermission(int level) + { + return this; + } + + @Override + public CommandSourceStack withAnchor(EntityAnchorArgument.Anchor anchor) + { + return new SnoopyCommandSource(output, position, rotation, world, level, simpleName, name, server, entity, resultConsumer, anchor, signingContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack withSigningContext(CommandSigningContext commandSigningContext, TaskChainer taskChainer) + { + return new SnoopyCommandSource(output, position, rotation, world, level, simpleName, name, server, entity, resultConsumer, entityAnchor, commandSigningContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack withLevel(ServerLevel world) + { + double d = DimensionType.getTeleportationScale(this.world.dimensionType(), world.dimensionType()); + Vec3 position = new Vec3(this.position.x * d, this.position.y, this.position.z * d); + return new SnoopyCommandSource(output, position, rotation, world, level, simpleName, name, server, entity, resultConsumer, entityAnchor, signingContext, taskChainer, error, chatOutput); + } + + @Override + public CommandSourceStack facing(Vec3 position) + { + Vec3 vec3d = this.entityAnchor.apply(this); + double d = position.x - vec3d.x; + double e = position.y - vec3d.y; + double f = position.z - vec3d.z; + double g = Math.sqrt(d * d + f * f); + float h = Mth.wrapDegrees((float) (-(Mth.atan2(e, g) * 57.2957763671875D))); + float i = Mth.wrapDegrees((float) (Mth.atan2(f, d) * 57.2957763671875D) - 90.0F); + return this.withRotation(new Vec2(h, i)); + } + + @Override + public void sendFailure(Component message) + { + error[0] = message; + } + + @Override + public void sendSuccess(Supplier message, boolean broadcastToOps) + { + chatOutput.add(message.get()); + } + +} diff --git a/src/main/java/carpet/script/utils/SystemInfo.java b/src/main/java/carpet/script/utils/SystemInfo.java new file mode 100644 index 0000000..0174d90 --- /dev/null +++ b/src/main/java/carpet/script/utils/SystemInfo.java @@ -0,0 +1,200 @@ +package carpet.script.utils; + +import carpet.script.CarpetContext; +import carpet.script.CarpetScriptHost; +import carpet.script.external.Carpet; +import carpet.script.external.Vanilla; +import carpet.script.value.BooleanValue; +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 carpet.script.value.ValueConversions; +import com.sun.management.OperatingSystemMXBean; +import net.minecraft.SharedConstants; +import net.minecraft.server.packs.PackType; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.phys.Vec2; + +import java.lang.management.ManagementFactory; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class SystemInfo +{ + private static final Map> options = new HashMap<>() + {{ + put("app_name", c -> + { + String name = c.host.getName(); + return name == null ? Value.NULL : new StringValue(name); + }); + put("app_list", c -> ListValue.wrap(((CarpetScriptHost) c.host).scriptServer().modules.keySet().stream().filter(Objects::nonNull).map(StringValue::new))); + put("app_scope", c -> StringValue.of((c.host).isPerUser() ? "player" : "global")); + put("app_players", c -> ListValue.wrap(c.host.getUserList().stream().map(StringValue::new))); + + put("world_name", c -> new StringValue(c.server().getWorldData().getLevelName())); + put("world_seed", c -> new NumericValue(c.level().getSeed())); + put("server_motd", c -> StringValue.of(c.server().getMotd())); + put("world_path", c -> StringValue.of(c.server().getWorldPath(LevelResource.ROOT).toString())); + put("world_folder", c -> { + Path serverPath = c.server().getWorldPath(LevelResource.ROOT); + int nodeCount = serverPath.getNameCount(); + if (nodeCount < 2) + { + return Value.NULL; + } + String tlf = serverPath.getName(nodeCount - 2).toString(); + return StringValue.of(tlf); + }); + put("world_dimensions", c -> ListValue.wrap(c.server().levelKeys().stream().map(k -> ValueConversions.of(k.location())))); + put("world_spawn_point", c -> ValueConversions.of(c.server().overworld().getLevelData().getSpawnPos())); + + put("world_bottom", c -> new NumericValue(c.level().getMinBuildHeight())); + + put("world_top", c -> new NumericValue(c.level().getMaxBuildHeight())); + + put("world_center", c -> { + WorldBorder worldBorder = c.level().getWorldBorder(); + return ListValue.fromTriple(worldBorder.getCenterX(), 0, worldBorder.getCenterZ()); + }); + + put("world_size", c -> new NumericValue(c.level().getWorldBorder().getSize() / 2)); + + put("world_max_size", c -> new NumericValue(c.level().getWorldBorder().getAbsoluteMaxSize())); + + put("world_time", c -> new NumericValue(c.level().getGameTime())); + + put("game_difficulty", c -> StringValue.of(c.server().getWorldData().getDifficulty().getKey())); + put("game_hardcore", c -> BooleanValue.of(c.server().getWorldData().isHardcore())); + put("game_storage_format", c -> StringValue.of(c.server().getWorldData().getStorageVersionName(c.server().getWorldData().getVersion()))); + put("game_default_gamemode", c -> StringValue.of(c.server().getDefaultGameType().getName())); + put("game_max_players", c -> new NumericValue(c.server().getMaxPlayers())); + put("game_view_distance", c -> new NumericValue(c.server().getPlayerList().getViewDistance())); + put("game_mod_name", c -> StringValue.of(c.server().getServerModName())); + put("game_version", c -> StringValue.of(c.server().getServerVersion())); + put("game_target", c -> StringValue.of(String.format("1.%d.%d", + Vanilla.MinecraftServer_getReleaseTarget(c.server())[0], + Vanilla.MinecraftServer_getReleaseTarget(c.server())[1]))); + put("game_protocol", c -> NumericValue.of(SharedConstants.getProtocolVersion())); + put("game_major_target", c -> NumericValue.of(Vanilla.MinecraftServer_getReleaseTarget(c.server())[0])); + put("game_minor_target", c -> NumericValue.of(Vanilla.MinecraftServer_getReleaseTarget(c.server())[1])); + put("game_stable", c -> BooleanValue.of(SharedConstants.getCurrentVersion().isStable())); + put("game_data_version", c -> NumericValue.of(SharedConstants.getCurrentVersion().getDataVersion().getVersion())); + put("game_pack_version", c -> NumericValue.of(SharedConstants.getCurrentVersion().getPackVersion(PackType.SERVER_DATA))); + + put("server_ip", c -> StringValue.of(c.server().getLocalIp())); + put("server_whitelisted", c -> BooleanValue.of(c.server().isEnforceWhitelist())); + put("server_whitelist", c -> { + MapValue whitelist = new MapValue(Collections.emptyList()); + for (String s : c.server().getPlayerList().getWhiteListNames()) + { + whitelist.append(StringValue.of(s)); + } + return whitelist; + }); + put("server_banned_players", c -> { + MapValue whitelist = new MapValue(Collections.emptyList()); + for (String s : c.server().getPlayerList().getBans().getUserList()) + { + whitelist.append(StringValue.of(s)); + } + return whitelist; + }); + put("server_banned_ips", c -> { + MapValue whitelist = new MapValue(Collections.emptyList()); + for (String s : c.server().getPlayerList().getIpBans().getUserList()) + { + whitelist.append(StringValue.of(s)); + } + return whitelist; + }); + put("server_dev_environment", c -> BooleanValue.of(Vanilla.isDevelopmentEnvironment())); + put("server_mods", c -> Vanilla.getServerMods(c.server())); + put("server_last_tick_times", c -> { + //assuming we are in the tick world section + // might be off one tick when run in the off tasks or asynchronously. + int currentReportedTick = c.server().getTickCount() - 1; + List ticks = new ArrayList<>(100); + long[] tickArray = c.server().getTickTimesNanos(); + for (int i = currentReportedTick + 100; i > currentReportedTick; i--) + { + ticks.add(new NumericValue((tickArray[i % 100]) / 1000000.0)); + } + return ListValue.wrap(ticks); + }); + + put("java_max_memory", c -> new NumericValue(Runtime.getRuntime().maxMemory())); + put("java_allocated_memory", c -> new NumericValue(Runtime.getRuntime().totalMemory())); + put("java_used_memory", c -> new NumericValue(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())); + put("java_cpu_count", c -> new NumericValue(Runtime.getRuntime().availableProcessors())); + put("java_version", c -> StringValue.of(System.getProperty("java.version"))); + put("java_bits", c -> { + for (String property : new String[]{"sun.arch.data.model", "com.ibm.vm.bitmode", "os.arch"}) + { + String value = System.getProperty(property); + if (value != null && value.contains("64")) + { + return new NumericValue(64); + } + } + return new NumericValue(32); + }); + put("java_system_cpu_load", c -> { + OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean( + OperatingSystemMXBean.class); + return new NumericValue(osBean.getCpuLoad()); + }); + put("java_process_cpu_load", c -> { + OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean( + OperatingSystemMXBean.class); + return new NumericValue(osBean.getProcessCpuLoad()); + }); + put("world_carpet_rules", c -> Carpet.getAllCarpetRules()); + put("world_gamerules", c -> { + Map rules = new HashMap<>(); + GameRules gameRules = c.level().getGameRules(); + GameRules.visitGameRuleTypes(new GameRules.GameRuleTypeVisitor() + { + @Override + public > void visit(GameRules.Key key, GameRules.Type type) + { + rules.put(StringValue.of(key.getId()), StringValue.of(gameRules.getRule(key).toString())); + } + }); + return MapValue.wrap(rules); + }); + put("world_min_spawning_light", c -> NumericValue.of(c.level().dimensionType().monsterSpawnBlockLightLimit())); + + put("source_entity", c -> EntityValue.of(c.source().getEntity())); + put("source_position", c -> ValueConversions.of(c.source().getPosition())); + put("source_dimension", c -> ValueConversions.of(c.level())); + put("source_rotation", c -> { + Vec2 rotation = c.source().getRotation(); + return ListValue.of(new NumericValue(rotation.x), new NumericValue(rotation.y)); + }); + put("scarpet_version", c -> StringValue.of(Carpet.getCarpetVersion())); + }}; + + public static Value get(String what, CarpetContext cc) + { + return options.getOrDefault(what, c -> null).apply(cc); + } + + public static Value getAll() + { + return ListValue.wrap(options.keySet().stream().map(StringValue::of)); + } + +} diff --git a/src/main/java/carpet/script/utils/Tracer.java b/src/main/java/carpet/script/utils/Tracer.java new file mode 100644 index 0000000..a1e17d1 --- /dev/null +++ b/src/main/java/carpet/script/utils/Tracer.java @@ -0,0 +1,90 @@ +package carpet.script.utils; + +import java.util.Optional; +import java.util.function.Predicate; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +public class Tracer +{ + public static HitResult rayTrace(Entity source, float partialTicks, double reach, boolean fluids) + { + BlockHitResult blockHit = rayTraceBlocks(source, partialTicks, reach, fluids); + double maxSqDist = reach * reach; + if (blockHit != null) + { + maxSqDist = blockHit.getLocation().distanceToSqr(source.getEyePosition(partialTicks)); + } + EntityHitResult entityHit = rayTraceEntities(source, partialTicks, reach, maxSqDist); + return entityHit == null ? blockHit : entityHit; + } + + public static BlockHitResult rayTraceBlocks(Entity source, float partialTicks, double reach, boolean fluids) + { + Vec3 pos = source.getEyePosition(partialTicks); + Vec3 rotation = source.getViewVector(partialTicks); + Vec3 reachEnd = pos.add(rotation.x * reach, rotation.y * reach, rotation.z * reach); + return source.level().clip(new ClipContext(pos, reachEnd, ClipContext.Block.OUTLINE, fluids ? + ClipContext.Fluid.ANY : ClipContext.Fluid.NONE, source)); + } + + public static EntityHitResult rayTraceEntities(Entity source, float partialTicks, double reach, double maxSqDist) + { + Vec3 pos = source.getEyePosition(partialTicks); + Vec3 reachVec = source.getViewVector(partialTicks).scale(reach); + AABB box = source.getBoundingBox().expandTowards(reachVec).inflate(1); + return rayTraceEntities(source, pos, pos.add(reachVec), box, e -> !e.isSpectator() && e.isPickable(), maxSqDist); + } + + public static EntityHitResult rayTraceEntities(Entity source, Vec3 start, Vec3 end, AABB box, Predicate predicate, double maxSqDistance) + { + Level world = source.level(); + double targetDistance = maxSqDistance; + Entity target = null; + Vec3 targetHitPos = null; + for (Entity current : world.getEntities(source, box, predicate)) + { + AABB currentBox = current.getBoundingBox().inflate(current.getPickRadius()); + Optional currentHit = currentBox.clip(start, end); + if (currentBox.contains(start)) + { + if (targetDistance >= 0) + { + target = current; + targetHitPos = currentHit.orElse(start); + targetDistance = 0; + } + } + else if (currentHit.isPresent()) + { + Vec3 currentHitPos = currentHit.get(); + double currentDistance = start.distanceToSqr(currentHitPos); + if (currentDistance < targetDistance || targetDistance == 0) + { + if (current.getRootVehicle() == source.getRootVehicle()) + { + if (targetDistance == 0) + { + target = current; + targetHitPos = currentHitPos; + } + } + else + { + target = current; + targetHitPos = currentHitPos; + targetDistance = currentDistance; + } + } + } + } + return target == null ? null : new EntityHitResult(target, targetHitPos); + } +} diff --git a/src/main/java/carpet/script/utils/WorldTools.java b/src/main/java/carpet/script/utils/WorldTools.java new file mode 100644 index 0000000..15f23f7 --- /dev/null +++ b/src/main/java/carpet/script/utils/WorldTools.java @@ -0,0 +1,204 @@ +package carpet.script.utils; + +//import carpet.fakes.MinecraftServerInterface; +import carpet.script.external.Vanilla; +//import net.fabricmc.api.EnvType; +//import net.fabricmc.api.Environment; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.MappedRegistry; +import net.minecraft.core.Registry; +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.progress.ChunkProgressListener; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.CustomSpawner; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.BiomeManager; +import net.minecraft.world.level.biome.MultiNoiseBiomeSource; +import net.minecraft.world.level.border.BorderChangeListener; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.WorldGenSettings; +import net.minecraft.world.level.storage.DerivedLevelData; +import net.minecraft.world.level.storage.ServerLevelData; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +public class WorldTools +{ + + public static boolean canHasChunk(ServerLevel world, ChunkPos chpos, @Nullable Map regionCache, boolean deepcheck) + { + if (world.getChunk(chpos.x, chpos.z, ChunkStatus.STRUCTURE_STARTS, false) != null) + { + return true; + } + String currentRegionName = "r." + chpos.getRegionX() + "." + chpos.getRegionZ() + ".mca"; + if (regionCache != null && regionCache.containsKey(currentRegionName)) + { + RegionFile region = regionCache.get(currentRegionName); + if (region == null) + { + return false; + } + return region.hasChunk(chpos); + } + Path regionsFolder = Vanilla.MinecraftServer_storageSource(world.getServer()).getDimensionPath(world.dimension()).resolve("region"); + Path regionPath = regionsFolder.resolve(currentRegionName); + if (!regionPath.toFile().exists()) + { + if (regionCache != null) + { + regionCache.put(currentRegionName, null); + } + return false; + } + if (!deepcheck) + { + return true; // not using cache in this case. + } + try + { + RegionStorageInfo levelStorageInfo = new RegionStorageInfo(Vanilla.MinecraftServer_storageSource(world.getServer()).getLevelId(), world.dimension(), "chunk"); + RegionFile region = new RegionFile(levelStorageInfo, regionPath, regionsFolder, true); + if (regionCache != null) + { + regionCache.put(currentRegionName, region); + } + return region.hasChunk(chpos); + } + catch (IOException ignored) + { + } + return true; + } +/* + public static boolean createWorld(MinecraftServer server, String worldKey, Long seed) + { + ResourceLocation worldId = new ResourceLocation(worldKey); + ServerLevel overWorld = server.overworld(); + + Set> worldKeys = server.levelKeys(); + for (ResourceKey worldRegistryKey : worldKeys) + { + if (worldRegistryKey.location().equals(worldId)) + { + // world with this id already exists + return false; + } + } + ServerLevelData serverWorldProperties = server.getWorldData().overworldData(); + WorldGenSettings generatorOptions = server.getWorldData().worldGenSettings(); + boolean bl = generatorOptions.isDebug(); + long l = generatorOptions.seed(); + long m = BiomeManager.obfuscateSeed(l); + List list = List.of(); + Registry simpleRegistry = generatorOptions.dimensions(); + LevelStem dimensionOptions = simpleRegistry.get(LevelStem.OVERWORLD); + ChunkGenerator chunkGenerator2; + Holder dimensionType2; + if (dimensionOptions == null) { + dimensionType2 = server.registryAccess().registryOrThrow(Registry.DIMENSION_TYPE_REGISTRY).getOrCreateHolder(DimensionType.OVERWORLD_LOCATION);; + chunkGenerator2 = WorldGenSettings.makeDefaultOverworld(server.registryAccess(), (new Random()).nextLong()); + } else { + dimensionType2 = dimensionOptions.typeHolder(); + chunkGenerator2 = dimensionOptions.generator(); + } + + ResourceKey customWorld = ResourceKey.create(Registry.DIMENSION_REGISTRY, worldId); + + //chunkGenerator2 = GeneratorOptions.createOverworldGenerator(server.getRegistryManager().get(Registry.BIOME_KEY), server.getRegistryManager().get(Registry.NOISE_SETTINGS_WORLDGEN), (seed==null)?l:seed); + + // from world/gen/GeneratorOptions + //chunkGenerator2 = new NoiseChunkGenerator(MultiNoiseBiomeSource.createVanillaSource(server.getRegistryManager().get(Registry.BIOME_KEY), seed), seed, () -> { + // return server.getRegistryManager().get(Registry.CHUNK_GENERATOR_SETTINGS_KEY).getOrThrow(ChunkGeneratorSettings.OVERWORLD); + //}); + + chunkGenerator2 = new NoiseBasedChunkGenerator( + server.registryAccess().registryOrThrow(Registry.STRUCTURE_SET_REGISTRY), + server.registryAccess().registryOrThrow(Registry.NOISE_REGISTRY), + MultiNoiseBiomeSource.Preset.OVERWORLD.biomeSource(server.registryAccess().registryOrThrow(Registry.BIOME_REGISTRY)), seed, + Holder.direct(server.registryAccess().registryOrThrow(Registry.NOISE_GENERATOR_SETTINGS_REGISTRY).getOrThrow(NoiseGeneratorSettings.OVERWORLD)) + ); + + ServerLevel serverWorld = new ServerLevel( + server, + Util.backgroundExecutor(), + ((MinecraftServerInterface) server).getCMSession(), + new DerivedLevelData(server.getWorldData(), serverWorldProperties), + customWorld, + dimensionType2, + NOOP_LISTENER, + chunkGenerator2, + bl, + (seed==null)?l:seed, + list, + false); + overWorld.getWorldBorder().addListener(new BorderChangeListener.DelegateBorderChangeListener(serverWorld.getWorldBorder())); + ((MinecraftServerInterface) server).getCMWorlds().put(customWorld, serverWorld); + return true; + }*/ + + public static void forceChunkUpdate(BlockPos pos, ServerLevel world) + { + ChunkPos chunkPos = new ChunkPos(pos); + LevelChunk worldChunk = world.getChunkSource().getChunk(chunkPos.x, chunkPos.z, false); + if (worldChunk != null) + { + List players = world.getChunkSource().chunkMap.getPlayers(chunkPos, false); + if (!players.isEmpty()) + { + ClientboundLevelChunkWithLightPacket packet = new ClientboundLevelChunkWithLightPacket(worldChunk, world.getLightEngine(), null, null); // false seems to update neighbours as well. + players.forEach(p -> p.connection.send(packet)); + } + } + } + +/* + private static class NoopWorldGenerationProgressListener implements ChunkProgressListener + { + @Override + public void updateSpawnPos(final ChunkPos spawnPos) + { + } + + @Override + public void onStatusChange(final ChunkPos pos, final ChunkStatus status) + { + } + + //@Environment(EnvType.CLIENT) + @Override + public void start() + { + } + + @Override + public void stop() + { + } + } + + public static final ChunkProgressListener NOOP_LISTENER = new NoopWorldGenerationProgressListener(); + + */ +} diff --git a/src/main/java/carpet/script/utils/package-info.java b/src/main/java/carpet/script/utils/package-info.java new file mode 100644 index 0000000..61a3a27 --- /dev/null +++ b/src/main/java/carpet/script/utils/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.utils; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/utils/shapes/ShapeDirection.java b/src/main/java/carpet/script/utils/shapes/ShapeDirection.java new file mode 100644 index 0000000..29bc08a --- /dev/null +++ b/src/main/java/carpet/script/utils/shapes/ShapeDirection.java @@ -0,0 +1,33 @@ +package carpet.script.utils.shapes; + +import javax.annotation.Nullable; +import java.util.Locale; + +public enum ShapeDirection +{ + NORTH, + SOUTH, + EAST, + WEST, + UP, + DOWN, + CAMERA, + PLAYER; + + @Nullable + public static ShapeDirection fromString(String direction) + { + return switch (direction.toLowerCase(Locale.ROOT)) + { + case "north" -> NORTH; + case "south" -> SOUTH; + case "east" -> EAST; + case "west" -> WEST; + case "up" -> UP; + case "down" -> DOWN; + case "camera" -> CAMERA; + case "player" -> PLAYER; + default -> null; + }; + } +} diff --git a/src/main/java/carpet/script/utils/shapes/package-info.java b/src/main/java/carpet/script/utils/shapes/package-info.java new file mode 100644 index 0000000..1055bff --- /dev/null +++ b/src/main/java/carpet/script/utils/shapes/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.utils.shapes; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/script/value/AbstractListValue.java b/src/main/java/carpet/script/value/AbstractListValue.java new file mode 100644 index 0000000..b9d9176 --- /dev/null +++ b/src/main/java/carpet/script/value/AbstractListValue.java @@ -0,0 +1,39 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import com.google.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbstractListValue extends Value implements Iterable +{ + public List unpack() + { + ArrayList retVal = Lists.newArrayList(); + for (Value value : this) + { + if (value != Value.EOL) + { + retVal.add(value); + } + } + fatality(); + return retVal; + } + + public void fatality() + { + } + + public void append(Value v) + { + throw new InternalExpressionException("Cannot append a value to an abstract list"); + } + + @Override + public Value fromConstant() + { + return this.deepcopy(); + } +} diff --git a/src/main/java/carpet/script/value/BlockValue.java b/src/main/java/carpet/script/value/BlockValue.java new file mode 100644 index 0000000..77fef2f --- /dev/null +++ b/src/main/java/carpet/script/value/BlockValue.java @@ -0,0 +1,397 @@ +package carpet.script.value; + +import carpet.script.CarpetContext; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; + +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import net.minecraft.commands.arguments.blocks.BlockStateParser; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Direction.Axis; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +import static carpet.script.value.NBTSerializableValue.nameFromResource; + +public class BlockValue extends Value +{ + private BlockState blockState; + private final BlockPos pos; + private final ServerLevel world; + private CompoundTag data; + + // we only care for null values a few times, most of the time we would assume its all present + public static final BlockValue NONE = new BlockValue(Blocks.AIR.defaultBlockState(), null, BlockPos.ZERO, null); + + public static BlockValue fromCoords(CarpetContext c, int x, int y, int z) + { + BlockPos pos = locateBlockPos(c, x, y, z); + return new BlockValue(null, c.level(), pos); + } + + private static final Map bvCache = new HashMap<>(); + + public static BlockValue fromString(String str, ServerLevel level) + { + try + { + BlockValue bv = bvCache.get(str); // [SCARY SHIT] persistent caches over server reloads + if (bv != null) + { + return bv; + } + BlockStateParser.BlockResult foo = BlockStateParser.parseForBlock(level.registryAccess().lookupOrThrow(Registries.BLOCK), new StringReader(str), true); + if (foo.blockState() != null) + { + CompoundTag bd = foo.nbt(); + if (bd == null) + { + bd = new CompoundTag(); + } + bv = new BlockValue(foo.blockState(), level, null, bd); + if (bvCache.size() > 10000) + { + bvCache.clear(); + } + bvCache.put(str, bv); + return bv; + } + } + catch (CommandSyntaxException ignored) + { + } + throw new ThrowStatement(str, Throwables.UNKNOWN_BLOCK); + } + + public static BlockPos locateBlockPos(CarpetContext c, int xpos, int ypos, int zpos) + { + BlockPos pos = c.origin(); + return new BlockPos(pos.getX() + xpos, pos.getY() + ypos, pos.getZ() + zpos); + } + + public BlockState getBlockState() + { + if (blockState != null) + { + return blockState; + } + if (pos != null) + { + blockState = world.getBlockState(pos); + return blockState; + } + throw new InternalExpressionException("Attempted to fetch block state without world or stored block state"); + } + + public static BlockEntity getBlockEntity(Level level, BlockPos pos) + { + if (level instanceof final ServerLevel serverLevel) + { + return serverLevel.getServer().isSameThread() + ? serverLevel.getBlockEntity(pos) + : serverLevel.getChunkAt(pos).getBlockEntity(pos, LevelChunk.EntityCreationType.IMMEDIATE); + } + return null; + } + + + public CompoundTag getData() + { + if (data != null) + { + return data.isEmpty() ? null : data; + } + if (pos != null) + { + BlockEntity be = getBlockEntity(world, pos); + if (be == null) + { + data = new CompoundTag(); + return null; + } + data = be.saveWithoutMetadata(be.getLevel().registryAccess()); + return data; + } + return null; + } + + + public BlockValue(BlockState state, ServerLevel world, BlockPos position) + { + this.world = world; + blockState = state; + pos = position; + data = null; + } + + public BlockValue(BlockState state) + { + this.world = null; + blockState = state; + pos = null; + data = null; + } + + public BlockValue(ServerLevel world, BlockPos position) + { + this.world = world; + blockState = null; + pos = position; + data = null; + } + + public BlockValue(BlockState state, CompoundTag nbt) + { + this.world = null; + blockState = state; + pos = null; + data = nbt; + } + + public BlockValue(BlockState state, ServerLevel world, CompoundTag nbt) + { + this.world = world; + blockState = state; + pos = null; + data = nbt; + } + + private BlockValue(@Nullable BlockState state, @Nullable ServerLevel world, @Nullable BlockPos position, @Nullable CompoundTag nbt) + { + this.world = world; + blockState = state; + pos = position; + data = nbt; + } + + + @Override + public String getString() + { + Registry blockRegistry = world.registryAccess().registryOrThrow(Registries.BLOCK); + return nameFromResource(blockRegistry.getKey(getBlockState().getBlock())); + } + + @Override + public boolean getBoolean() + { + return !getBlockState().isAir(); + } + + @Override + public String getTypeString() + { + return "block"; + } + + @Override + public Value clone() + { + return new BlockValue(blockState, world, pos, data); + } + + @Override + public int hashCode() + { + return pos != null + ? GlobalPos.of(world.dimension(), pos).hashCode() + : ("b" + getString()).hashCode(); + } + + public BlockPos getPos() + { + return pos; + } + + public Level getWorld() + { + return world; + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + // follows falling block convertion + CompoundTag tag = new CompoundTag(); + CompoundTag state = new CompoundTag(); + BlockState s = getBlockState(); + state.put("Name", StringTag.valueOf(world.registryAccess().registryOrThrow(Registries.BLOCK).getKey(s.getBlock()).toString())); + Collection> properties = s.getProperties(); + if (!properties.isEmpty()) + { + CompoundTag props = new CompoundTag(); + for (Property p : properties) + { + props.put(p.getName(), StringTag.valueOf(s.getValue(p).toString().toLowerCase(Locale.ROOT))); + } + state.put("Properties", props); + } + tag.put("BlockState", state); + CompoundTag dataTag = getData(); + if (dataTag != null) + { + tag.put("TileEntityData", dataTag); + } + return tag; + } + + public enum SpecificDirection + { + UP("up", 0.5, 0.0, 0.5, Direction.UP), + + UPNORTH("up-north", 0.5, 0.0, 0.4, Direction.UP), + UPSOUTH("up-south", 0.5, 0.0, 0.6, Direction.UP), + UPEAST("up-east", 0.6, 0.0, 0.5, Direction.UP), + UPWEST("up-west", 0.4, 0.0, 0.5, Direction.UP), + + DOWN("down", 0.5, 1.0, 0.5, Direction.DOWN), + + DOWNNORTH("down-north", 0.5, 1.0, 0.4, Direction.DOWN), + DOWNSOUTH("down-south", 0.5, 1.0, 0.6, Direction.DOWN), + DOWNEAST("down-east", 0.6, 1.0, 0.5, Direction.DOWN), + DOWNWEST("down-west", 0.4, 1.0, 0.5, Direction.DOWN), + + + NORTH("north", 0.5, 0.4, 1.0, Direction.NORTH), + SOUTH("south", 0.5, 0.4, 0.0, Direction.SOUTH), + EAST("east", 0.0, 0.4, 0.5, Direction.EAST), + WEST("west", 1.0, 0.4, 0.5, Direction.WEST), + + NORTHUP("north-up", 0.5, 0.6, 1.0, Direction.NORTH), + SOUTHUP("south-up", 0.5, 0.6, 0.0, Direction.SOUTH), + EASTUP("east-up", 0.0, 0.6, 0.5, Direction.EAST), + WESTUP("west-up", 1.0, 0.6, 0.5, Direction.WEST); + + public final String name; + public final Vec3 hitOffset; + public final Direction facing; + + private static final Map DIRECTION_MAP = Arrays.stream(values()).collect(Collectors.toMap(SpecificDirection::getName, d -> d)); + + + SpecificDirection(String name, double hitx, double hity, double hitz, Direction blockFacing) + { + this.name = name; + this.hitOffset = new Vec3(hitx, hity, hitz); + this.facing = blockFacing; + } + + private String getName() + { + return name; + } + } + + public static class PlacementContext extends BlockPlaceContext + { + private final Direction facing; + private final boolean sneakPlace; + + public static PlacementContext from(Level world, BlockPos pos, String direction, boolean sneakPlace, ItemStack itemStack) + { + SpecificDirection dir = SpecificDirection.DIRECTION_MAP.get(direction); + if (dir == null) + { + throw new InternalExpressionException("unknown block placement direction: " + direction); + } + BlockHitResult hitres = new BlockHitResult(Vec3.atLowerCornerOf(pos).add(dir.hitOffset), dir.facing, pos, false); + return new PlacementContext(world, dir.facing, sneakPlace, itemStack, hitres); + } + + private PlacementContext(Level world_1, Direction direction_1, boolean sneakPlace, ItemStack itemStack_1, BlockHitResult hitres) + { + super(world_1, null, InteractionHand.MAIN_HAND, itemStack_1, hitres); + this.facing = direction_1; + this.sneakPlace = sneakPlace; + } + + @Override + public BlockPos getClickedPos() + { + boolean prevcanReplaceExisting = replaceClicked; + replaceClicked = true; + BlockPos ret = super.getClickedPos(); + replaceClicked = prevcanReplaceExisting; + return ret; + } + + @Override + public Direction getNearestLookingDirection() + { + return facing.getOpposite(); + } + + @Override + public Direction[] getNearestLookingDirections() + { + return switch (this.facing) + { + case DOWN -> new Direction[]{Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST, Direction.UP}; + case UP -> new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST}; + case NORTH -> new Direction[]{Direction.DOWN, Direction.NORTH, Direction.EAST, Direction.WEST, Direction.UP, Direction.SOUTH}; + case SOUTH -> new Direction[]{Direction.DOWN, Direction.SOUTH, Direction.EAST, Direction.WEST, Direction.UP, Direction.NORTH}; + case WEST -> new Direction[]{Direction.DOWN, Direction.WEST, Direction.SOUTH, Direction.UP, Direction.NORTH, Direction.EAST}; + case EAST -> new Direction[]{Direction.DOWN, Direction.EAST, Direction.SOUTH, Direction.UP, Direction.NORTH, Direction.WEST}; + }; + } + + @Override + public Direction getHorizontalDirection() + { + return this.facing.getAxis() == Direction.Axis.Y ? Direction.NORTH : this.facing; + } + + @Override + public Direction getNearestLookingVerticalDirection() + { + return facing.getAxis() == Axis.Y ? facing : Direction.UP; + } + + @Override + public boolean isSecondaryUseActive() + { + return sneakPlace; + } + + @Override + public float getRotation() + { + return (float) (this.facing.get2DDataValue() * 90); + } + } +} diff --git a/src/main/java/carpet/script/value/BooleanValue.java b/src/main/java/carpet/script/value/BooleanValue.java new file mode 100644 index 0000000..8873af3 --- /dev/null +++ b/src/main/java/carpet/script/value/BooleanValue.java @@ -0,0 +1,74 @@ +package carpet.script.value; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.RegistryAccess; +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 ? 1L : 0L); + boolValue = boolval; + } + + public static BooleanValue of(boolean value) + { + return value ? TRUE : FALSE; + } + + @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, RegistryAccess regs) + { + return ByteTag.valueOf(boolValue); + } + + @Override + public JsonElement toJson() + { + return new JsonPrimitive(boolValue); + } + + @Override + public boolean isInteger() + { + return true; + } +} diff --git a/src/main/java/carpet/script/value/ContainerValueInterface.java b/src/main/java/carpet/script/value/ContainerValueInterface.java new file mode 100644 index 0000000..86db50d --- /dev/null +++ b/src/main/java/carpet/script/value/ContainerValueInterface.java @@ -0,0 +1,17 @@ +package carpet.script.value; + +public interface ContainerValueInterface +{ + boolean put(Value where, Value value); + + default boolean put(Value where, Value value, Value conditions) + { + return put(where, value); + } + + Value get(Value where); + + boolean has(Value where); + + boolean delete(Value where); +} diff --git a/src/main/java/carpet/script/value/EntityValue.java b/src/main/java/carpet/script/value/EntityValue.java new file mode 100644 index 0000000..f3dcc1c --- /dev/null +++ b/src/main/java/carpet/script/value/EntityValue.java @@ -0,0 +1,1750 @@ +package carpet.script.value; + +import carpet.script.external.Vanilla; +import carpet.script.utils.Tracer; +import carpet.script.CarpetContext; +import carpet.script.CarpetScriptServer; +import carpet.script.EntityEventsGroup; +import carpet.script.argument.Vector3Argument; +import carpet.script.exception.InternalExpressionException; +import carpet.script.external.Carpet; +import carpet.script.utils.EntityTools; +import carpet.script.utils.InputValidator; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.selector.EntitySelector; +import net.minecraft.commands.arguments.selector.EntitySelectorParser; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket; +import net.minecraft.network.protocol.game.ClientboundSetExperiencePacket; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.tags.EntityTypeTags; +import net.minecraft.tags.TagKey; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.Pose; +import net.minecraft.world.entity.RelativeMovement; +import net.minecraft.world.entity.ai.Brain; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeMap; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.goal.MoveTowardsRestrictionGoal; +import net.minecraft.world.entity.ai.memory.ExpirableValue; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.animal.IronGolem; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.projectile.WitherSkull; +import net.minecraft.world.entity.vehicle.AbstractMinecart; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static carpet.script.value.NBTSerializableValue.nameFromRegistryId; + +// TODO: decide whether copy(entity) should duplicate entity in the world. +public class EntityValue extends Value +{ + private Entity entity; + + public EntityValue(Entity e) + { + entity = e; + } + + public static Value of(@Nullable Entity e) + { + return e == null ? Value.NULL : new EntityValue(e); + } + + private static final Map selectorCache = new HashMap<>(); + + public static Collection getEntitiesFromSelector(CommandSourceStack source, String selector) + { + try + { + EntitySelector entitySelector = selectorCache.get(selector); + if (entitySelector != null) + { + return entitySelector.findEntities(source.withMaximumPermission(4)); + } + entitySelector = new EntitySelectorParser(new StringReader(selector), true).parse(); + selectorCache.put(selector, entitySelector); + return entitySelector.findEntities(source.withMaximumPermission(4)); + } + catch (CommandSyntaxException e) + { + throw new InternalExpressionException("Cannot select entities from " + selector); + } + } + + public Entity getEntity() + { + if (entity instanceof ServerPlayer serverPlayer && Vanilla.ServerPlayer_isInvalidEntityObject(serverPlayer)) + { + ServerPlayer newPlayer = entity.getServer().getPlayerList().getPlayer(entity.getUUID()); + if (newPlayer != null) + { + entity = newPlayer; + } + } + return entity; + } + + public static ServerPlayer getPlayerByValue(MinecraftServer server, Value value) + { + if (value instanceof EntityValue ev && ev.getEntity() instanceof ServerPlayer sp) + { + return sp; + } + if (value.isNull()) + { + return null; + } + String playerName = value.getString(); + return server.getPlayerList().getPlayerByName(playerName); + } + + public static String getPlayerNameByValue(Value value) + { + if (value instanceof EntityValue ev && ev.getEntity() instanceof ServerPlayer sp) + { + return sp.getScoreboardName(); + } + if (value.isNull()) + { + return null; + } + return value.getString(); + } + + @Override + public String getString() + { + return getEntity().getName().getString(); + } + + @Override + public boolean getBoolean() + { + return true; + } + + @Override + public boolean equals(Object v) + { + if (v instanceof EntityValue ev) + { + return getEntity().getId() == ev.getEntity().getId(); + } + return super.equals(v); + } + + @Override + public Value in(Value v) + { + if (v instanceof ListValue lv) + { + List values = lv.getItems(); + String what = values.get(0).getString(); + Value arg = null; + if (values.size() == 2) + { + arg = values.get(1); + } + else if (values.size() > 2) + { + arg = ListValue.wrap(values.subList(1, values.size())); + } + return this.get(what, arg); + } + String what = v.getString(); + return this.get(what, null); + } + + @Override + public String getTypeString() + { + return "entity"; + } + + @Override + public int hashCode() + { + return getEntity().hashCode(); + } + + public static final EntityTypeTest ANY = EntityTypeTest.forClass(Entity.class); + + public static EntityClassDescriptor getEntityDescriptor(String who, MinecraftServer server) + { + EntityClassDescriptor eDesc = EntityClassDescriptor.byName.get(who); + if (eDesc == null) + { + boolean positive = true; + if (who.startsWith("!")) + { + positive = false; + who = who.substring(1); + } + String booWho = who; + HolderSet.Named> eTagValue = server.registryAccess().registryOrThrow(Registries.ENTITY_TYPE) + .getTag(TagKey.create(Registries.ENTITY_TYPE, InputValidator.identifierOf(who))) + .orElseThrow(() -> new InternalExpressionException(booWho + " is not a valid entity descriptor")); + Set> eTag = eTagValue.stream().map(Holder::value).collect(Collectors.toUnmodifiableSet()); + if (positive) + { + if (eTag.size() == 1) + { + EntityType type = eTag.iterator().next(); + return new EntityClassDescriptor(type, Entity::isAlive, eTag.stream()); + } + else + { + return new EntityClassDescriptor(ANY, e -> eTag.contains(e.getType()) && e.isAlive(), eTag.stream()); + } + } + else + { + return new EntityClassDescriptor(ANY, e -> !eTag.contains(e.getType()) && e.isAlive(), server.registryAccess().registryOrThrow(Registries.ENTITY_TYPE).stream().filter(et -> !eTag.contains(et))); + } + } + return eDesc; + //TODO add more here like search by tags, or type + //if (who.startsWith('tag:')) + } + + public static class EntityClassDescriptor + { + public final EntityTypeTest directType; // interface of EntityType + public final Predicate filteringPredicate; + public final List> types; + + EntityClassDescriptor(EntityTypeTest type, Predicate predicate, List> types) + { + this.directType = type; + this.filteringPredicate = predicate; + this.types = types; + } + + EntityClassDescriptor(EntityTypeTest type, Predicate predicate, Stream> types) + { + this(type, predicate, types.toList()); + } + + public Value listValue(RegistryAccess regs) + { + Registry> entityRegs = regs.registryOrThrow(Registries.ENTITY_TYPE); + return ListValue.wrap(types.stream().map(et -> nameFromRegistryId(entityRegs.getKey(et)))); + } + + public static final Map byName = new HashMap<>() + {{ + List> allTypes = BuiltInRegistries.ENTITY_TYPE.stream().toList(); + // nonliving types + Set> projectiles = Set.of( + EntityType.ARROW, EntityType.DRAGON_FIREBALL, EntityType.FIREWORK_ROCKET, + EntityType.FIREBALL, EntityType.LLAMA_SPIT, EntityType.SMALL_FIREBALL, + EntityType.SNOWBALL, EntityType.SPECTRAL_ARROW, EntityType.EGG, + EntityType.ENDER_PEARL, EntityType.EXPERIENCE_BOTTLE, EntityType.POTION, + EntityType.TRIDENT, EntityType.WITHER_SKULL, EntityType.FISHING_BOBBER, EntityType.SHULKER_BULLET + ); + Set> deads = Set.of( + EntityType.AREA_EFFECT_CLOUD, EntityType.MARKER, EntityType.BOAT, EntityType.END_CRYSTAL, + EntityType.EVOKER_FANGS, EntityType.EXPERIENCE_ORB, EntityType.EYE_OF_ENDER, + EntityType.FALLING_BLOCK, EntityType.ITEM, EntityType.ITEM_FRAME, EntityType.GLOW_ITEM_FRAME, + EntityType.LEASH_KNOT, EntityType.LIGHTNING_BOLT, EntityType.PAINTING, + EntityType.TNT, EntityType.ARMOR_STAND, EntityType.CHEST_BOAT + + ); + Set> minecarts = Set.of( + EntityType.MINECART, EntityType.CHEST_MINECART, EntityType.COMMAND_BLOCK_MINECART, + EntityType.FURNACE_MINECART, EntityType.HOPPER_MINECART, + EntityType.SPAWNER_MINECART, EntityType.TNT_MINECART + ); + // living mob groups - non-defeault + Set> undeads = Set.of( + EntityType.STRAY, EntityType.SKELETON, EntityType.WITHER_SKELETON, + EntityType.ZOMBIE, EntityType.DROWNED, EntityType.ZOMBIE_VILLAGER, + EntityType.ZOMBIE_HORSE, EntityType.SKELETON_HORSE, EntityType.PHANTOM, + EntityType.WITHER, EntityType.ZOGLIN, EntityType.HUSK, EntityType.ZOMBIFIED_PIGLIN + + ); + Set> arthropods = Set.of( + EntityType.BEE, EntityType.ENDERMITE, EntityType.SILVERFISH, EntityType.SPIDER, + EntityType.CAVE_SPIDER + ); + Set> aquatique = Set.of( + EntityType.GUARDIAN, EntityType.TURTLE, EntityType.COD, EntityType.DOLPHIN, EntityType.PUFFERFISH, + EntityType.SALMON, EntityType.SQUID, EntityType.TROPICAL_FISH + ); + Set> illagers = Set.of( + EntityType.PILLAGER, EntityType.ILLUSIONER, EntityType.VINDICATOR, EntityType.EVOKER, + EntityType.RAVAGER, EntityType.WITCH + ); + + Set> living = allTypes.stream().filter(et -> + !deads.contains(et) && !projectiles.contains(et) && !minecarts.contains(et) + ).collect(Collectors.toSet()); + + Set> regular = allTypes.stream().filter(et -> + living.contains(et) && !undeads.contains(et) && !arthropods.contains(et) && !aquatique.contains(et) && !illagers.contains(et) + ).collect(Collectors.toSet()); + + + put("*", new EntityClassDescriptor(ANY, e -> true, allTypes)); + put("valid", new EntityClassDescriptor(ANY, net.minecraft.world.entity.EntitySelector.ENTITY_STILL_ALIVE, allTypes)); + put("!valid", new EntityClassDescriptor(ANY, e -> !e.isAlive(), allTypes)); + + put("living", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), net.minecraft.world.entity.EntitySelector.ENTITY_STILL_ALIVE, allTypes.stream().filter(living::contains))); + put("!living", new EntityClassDescriptor(ANY, (e) -> (!(e instanceof LivingEntity) && e.isAlive()), allTypes.stream().filter(et -> !living.contains(et)))); + + put("projectile", new EntityClassDescriptor(EntityTypeTest.forClass(Projectile.class), net.minecraft.world.entity.EntitySelector.ENTITY_STILL_ALIVE, allTypes.stream().filter(projectiles::contains))); + put("!projectile", new EntityClassDescriptor(ANY, (e) -> (!(e instanceof Projectile) && e.isAlive()), allTypes.stream().filter(et -> !projectiles.contains(et) && !living.contains(et)))); + + put("minecarts", new EntityClassDescriptor(EntityTypeTest.forClass(AbstractMinecart.class), net.minecraft.world.entity.EntitySelector.ENTITY_STILL_ALIVE, allTypes.stream().filter(minecarts::contains))); + put("!minecarts", new EntityClassDescriptor(ANY, (e) -> (!(e instanceof AbstractMinecart) && e.isAlive()), allTypes.stream().filter(et -> !minecarts.contains(et) && !living.contains(et)))); + + + // combat groups + + put("arthropod", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (e.getType().is(EntityTypeTags.ARTHROPOD) && e.isAlive()), allTypes.stream().filter(arthropods::contains))); + put("!arthropod", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (!e.getType().is(EntityTypeTags.ARTHROPOD) && e.isAlive()), allTypes.stream().filter(et -> !arthropods.contains(et) && living.contains(et)))); + + put("undead", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (e.getType().is(EntityTypeTags.UNDEAD) && e.isAlive()), allTypes.stream().filter(undeads::contains))); + put("!undead", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (!e.getType().is(EntityTypeTags.UNDEAD) && e.isAlive()), allTypes.stream().filter(et -> !undeads.contains(et) && living.contains(et)))); + + put("aquatic", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (e.getType().is(EntityTypeTags.AQUATIC) && e.isAlive()), allTypes.stream().filter(aquatique::contains))); + put("!aquatic", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (!e.getType().is(EntityTypeTags.AQUATIC) && e.isAlive()), allTypes.stream().filter(et -> !aquatique.contains(et) && living.contains(et)))); + + put("illager", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (e.getType().is(EntityTypeTags.ILLAGER) && e.isAlive()), allTypes.stream().filter(illagers::contains))); + put("!illager", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> (!e.getType().is(EntityTypeTags.ILLAGER) && e.isAlive()), allTypes.stream().filter(et -> !illagers.contains(et) && living.contains(et)))); + + put("regular", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> { + EntityType type = e.getType(); + return !illagers.contains(type) && !arthropods.contains(type) && !undeads.contains(type) && !aquatique.contains(type) && living.contains(type) && e.isAlive(); + }, allTypes.stream().filter(regular::contains))); + put("!regular", new EntityClassDescriptor(EntityTypeTest.forClass(LivingEntity.class), e -> { + EntityType type = e.getType(); + return (illagers.contains(type) || arthropods.contains(type) || undeads.contains(type) || aquatique.contains(type)) && e.isAlive(); + }, allTypes.stream().filter(et -> !regular.contains(et) && living.contains(et)))); + + for (ResourceLocation typeId : BuiltInRegistries.ENTITY_TYPE.keySet()) + { + EntityType type = BuiltInRegistries.ENTITY_TYPE.get(typeId); + String mobType = ValueConversions.simplify(typeId); + put(mobType, new EntityClassDescriptor(type, net.minecraft.world.entity.EntitySelector.ENTITY_STILL_ALIVE, Stream.of(type))); + put("!" + mobType, new EntityClassDescriptor(ANY, (e) -> e.getType() != type && e.isAlive(), allTypes.stream().filter(et -> et != type))); + } + for (MobCategory catId : MobCategory.values()) + { + String catStr = catId.getName(); + put(catStr, new EntityClassDescriptor(ANY, e -> ((e.getType().getCategory() == catId) && e.isAlive()), allTypes.stream().filter(et -> et.getCategory() == catId))); + put("!" + catStr, new EntityClassDescriptor(ANY, e -> ((e.getType().getCategory() != catId) && e.isAlive()), allTypes.stream().filter(et -> et.getCategory() != catId))); + } + }}; + } + + public Value get(String what, @Nullable Value arg) + { + if (!(featureAccessors.containsKey(what))) + { + throw new InternalExpressionException("Unknown entity feature: " + what); + } + try + { + return featureAccessors.get(what).apply(getEntity(), arg); + } + catch (NullPointerException npe) + { + throw new InternalExpressionException("Cannot fetch '" + what + "' with these arguments"); + } + } + + private static final Map inventorySlots = Map.of( + "mainhand", EquipmentSlot.MAINHAND, + "offhand", EquipmentSlot.OFFHAND, + "head", EquipmentSlot.HEAD, + "chest", EquipmentSlot.CHEST, + "legs", EquipmentSlot.LEGS, + "feet", EquipmentSlot.FEET + ); + + private static final Map> featureAccessors = new HashMap>() + {{ + //put("test", (e, a) -> a == null ? Value.NULL : new StringValue(a.getString())); + put("removed", (entity, arg) -> BooleanValue.of(entity.isRemoved())); + put("uuid", (e, a) -> new StringValue(e.getStringUUID())); + put("id", (e, a) -> new NumericValue(e.getId())); + put("pos", (e, a) -> ListValue.of(new NumericValue(e.getX()), new NumericValue(e.getY()), new NumericValue(e.getZ()))); + put("location", (e, a) -> ListValue.of(new NumericValue(e.getX()), new NumericValue(e.getY()), new NumericValue(e.getZ()), new NumericValue(e.getYRot()), new NumericValue(e.getXRot()))); + put("x", (e, a) -> new NumericValue(e.getX())); + put("y", (e, a) -> new NumericValue(e.getY())); + put("z", (e, a) -> new NumericValue(e.getZ())); + put("motion", (e, a) -> + { + Vec3 velocity = e.getDeltaMovement(); + return ListValue.of(new NumericValue(velocity.x), new NumericValue(velocity.y), new NumericValue(velocity.z)); + }); + put("motion_x", (e, a) -> new NumericValue(e.getDeltaMovement().x)); + put("motion_y", (e, a) -> new NumericValue(e.getDeltaMovement().y)); + put("motion_z", (e, a) -> new NumericValue(e.getDeltaMovement().z)); + put("on_ground", (e, a) -> BooleanValue.of(e.onGround())); + put("name", (e, a) -> new StringValue(e.getName().getString())); + put("display_name", (e, a) -> new FormattedTextValue(e.getDisplayName())); + put("command_name", (e, a) -> new StringValue(e.getScoreboardName())); + put("custom_name", (e, a) -> e.hasCustomName() ? new StringValue(e.getCustomName().getString()) : Value.NULL); + put("type", (e, a) -> nameFromRegistryId(e.level().registryAccess().registryOrThrow(Registries.ENTITY_TYPE).getKey(e.getType()))); + put("is_riding", (e, a) -> BooleanValue.of(e.isPassenger())); + put("is_ridden", (e, a) -> BooleanValue.of(e.isVehicle())); + put("passengers", (e, a) -> ListValue.wrap(e.getPassengers().stream().map(EntityValue::new))); + put("mount", (e, a) -> (e.getVehicle() != null) ? new EntityValue(e.getVehicle()) : Value.NULL); + put("unmountable", (e, a) -> BooleanValue.of(Vanilla.Entity_isPermanentVehicle(e))); + // deprecated + put("tags", (e, a) -> ListValue.wrap(e.getTags().stream().map(StringValue::new))); + + put("scoreboard_tags", (e, a) -> ListValue.wrap(e.getTags().stream().map(StringValue::new))); + put("entity_tags", (e, a) -> { + EntityType type = e.getType(); + return ListValue.wrap(e.getServer().registryAccess().registryOrThrow(Registries.ENTITY_TYPE).getTags().filter(entry -> entry.getSecond().stream().anyMatch(h -> h.value() == type)).map(entry -> ValueConversions.of(entry.getFirst()))); + }); + // deprecated + put("has_tag", (e, a) -> BooleanValue.of(e.getTags().contains(a.getString()))); + + put("has_scoreboard_tag", (e, a) -> BooleanValue.of(e.getTags().contains(a.getString()))); + put("has_entity_tag", (e, a) -> { + Optional>> tag = e.getServer().registryAccess().registryOrThrow(Registries.ENTITY_TYPE).getTag(TagKey.create(Registries.ENTITY_TYPE, InputValidator.identifierOf(a.getString()))); + if (tag.isEmpty()) + { + return Value.NULL; + } + //Tag> tag = e.getServer().getTags().getOrEmpty(Registry.ENTITY_TYPE_REGISTRY).getTag(InputValidator.identifierOf(a.getString())); + //if (tag == null) return Value.NULL; + //return BooleanValue.of(e.getType().is(tag)); + EntityType type = e.getType(); + return BooleanValue.of(tag.get().stream().anyMatch(h -> h.value() == type)); + }); + + put("yaw", (e, a) -> new NumericValue(e.getYRot())); + put("head_yaw", (e, a) -> e instanceof LivingEntity ? new NumericValue(e.getYHeadRot()) : Value.NULL); + put("body_yaw", (e, a) -> e instanceof LivingEntity le ? new NumericValue(le.yBodyRot) : Value.NULL); + + put("pitch", (e, a) -> new NumericValue(e.getXRot())); + put("look", (e, a) -> { + Vec3 look = e.getLookAngle(); + return ListValue.of(new NumericValue(look.x), new NumericValue(look.y), new NumericValue(look.z)); + }); + put("is_burning", (e, a) -> BooleanValue.of(e.isOnFire())); + put("fire", (e, a) -> new NumericValue(e.getRemainingFireTicks())); + put("is_freezing", (e, a) -> BooleanValue.of(e.isFullyFrozen())); + put("frost", (e, a) -> new NumericValue(e.getTicksFrozen())); + put("silent", (e, a) -> BooleanValue.of(e.isSilent())); + put("gravity", (e, a) -> BooleanValue.of(!e.isNoGravity())); + put("immune_to_fire", (e, a) -> BooleanValue.of(e.fireImmune())); + put("immune_to_frost", (e, a) -> BooleanValue.of(!e.canFreeze())); + + put("invulnerable", (e, a) -> BooleanValue.of(e.isInvulnerable())); + put("dimension", (e, a) -> nameFromRegistryId(e.level().dimension().location())); // getDimId + put("height", (e, a) -> new NumericValue(e.getDimensions(Pose.STANDING).height())); + put("width", (e, a) -> new NumericValue(e.getDimensions(Pose.STANDING).width())); + put("eye_height", (e, a) -> new NumericValue(e.getEyeHeight())); + put("age", (e, a) -> new NumericValue(e.tickCount)); + put("breeding_age", (e, a) -> e instanceof AgeableMob am ? new NumericValue(am.getAge()) : Value.NULL); + put("despawn_timer", (e, a) -> e instanceof LivingEntity le ? new NumericValue(le.getNoActionTime()) : Value.NULL); + put("blue_skull", (e, a) -> e instanceof WitherSkull w ? BooleanValue.of(w.isDangerous()) : Value.NULL); + put("offering_flower", (e, a) -> e instanceof IronGolem ig ? BooleanValue.of(ig.getOfferFlowerTick() > 0) : Value.NULL); + put("item", (e, a) -> e instanceof ItemEntity ie ? ValueConversions.of(ie.getItem(), e.getServer().registryAccess()) : e instanceof ItemFrame frame ? ValueConversions.of(frame.getItem(), e.getServer().registryAccess()) : Value.NULL); + put("count", (e, a) -> (e instanceof ItemEntity ie) ? new NumericValue(ie.getItem().getCount()) : Value.NULL); + put("pickup_delay", (e, a) -> (e instanceof ItemEntity ie) ? new NumericValue(Vanilla.ItemEntity_getPickupDelay(ie)) : Value.NULL); + put("portal_cooldown", (e, a) -> new NumericValue(Vanilla.Entity_getPublicNetherPortalCooldown(e))); + put("portal_timer", (e, a) -> new NumericValue(Vanilla.Entity_getPortalTimer(e))); + // ItemEntity -> despawn timer via ssGetAge + put("is_baby", (e, a) -> (e instanceof LivingEntity le) ? BooleanValue.of(le.isBaby()) : Value.NULL); + put("target", (e, a) -> { + if (e instanceof Mob mob) + { + LivingEntity target = mob.getTarget(); // there is also getAttacking in living.... + if (target != null) + { + return new EntityValue(target); + } + } + return Value.NULL; + }); + put("home", (e, a) -> e instanceof Mob mob ? (mob.getRestrictRadius() > 0) ? new BlockValue(null, (ServerLevel) e.level(), mob.getRestrictCenter()) : Value.FALSE : Value.NULL); + put("spawn_point", (e, a) -> { + if (e instanceof ServerPlayer spe) + { + if (spe.getRespawnPosition() == null) + { + return Value.FALSE; + } + return ListValue.of( + ValueConversions.of(spe.getRespawnPosition()), + ValueConversions.of(spe.getRespawnDimension()), + new NumericValue(spe.getRespawnAngle()), + BooleanValue.of(spe.isRespawnForced()) + ); + } + return Value.NULL; + }); + put("pose", (e, a) -> new StringValue(e.getPose().name().toLowerCase(Locale.ROOT))); + put("sneaking", (e, a) -> e.isShiftKeyDown() ? 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("swinging", (e, a) -> e instanceof LivingEntity le ? BooleanValue.of(le.swinging) : Value.NULL); + put("air", (e, a) -> new NumericValue(e.getAirSupply())); + put("language", (e, a) -> !(e instanceof ServerPlayer p) ? NULL : StringValue.of(Vanilla.ServerPlayer_getLanguage(p))); + put("persistence", (e, a) -> e instanceof Mob mob ? BooleanValue.of(mob.isPersistenceRequired()) : Value.NULL); + put("hunger", (e, a) -> e instanceof Player player ? new NumericValue(player.getFoodData().getFoodLevel()) : Value.NULL); + put("saturation", (e, a) -> e instanceof Player player ? new NumericValue(player.getFoodData().getSaturationLevel()) : Value.NULL); + put("exhaustion", (e, a) -> e instanceof Player player ? new NumericValue(player.getFoodData().getExhaustionLevel()) : Value.NULL); + put("absorption", (e, a) -> e instanceof Player player ? new NumericValue(player.getAbsorptionAmount()) : Value.NULL); + put("xp", (e, a) -> e instanceof Player player ? new NumericValue(player.totalExperience) : Value.NULL); + put("xp_level", (e, a) -> e instanceof Player player ? new NumericValue(player.experienceLevel) : Value.NULL); + put("xp_progress", (e, a) -> e instanceof Player player ? new NumericValue(player.experienceProgress) : Value.NULL); + put("score", (e, a) -> e instanceof Player player ? new NumericValue(player.getScore()) : Value.NULL); + put("jumping", (e, a) -> e instanceof LivingEntity le ? Vanilla.LivingEntity_isJumping(le) ? Value.TRUE : Value.FALSE : Value.NULL); + put("gamemode", (e, a) -> e instanceof ServerPlayer sp ? new StringValue(sp.gameMode.getGameModeForPlayer().getName()) : Value.NULL); + put("path", (e, a) -> { + if (e instanceof Mob mob) + { + Path path = mob.getNavigation().getPath(); + if (path == null) + { + return Value.NULL; + } + return ValueConversions.fromPath((ServerLevel) e.getCommandSenderWorld(), path); + } + return Value.NULL; + }); + + put("brain", (e, a) -> { + String module = a.getString(); + MemoryModuleType moduleType = e.level().registryAccess().registryOrThrow(Registries.MEMORY_MODULE_TYPE).get(InputValidator.identifierOf(module)); + if (moduleType == MemoryModuleType.DUMMY) + { + return Value.NULL; + } + if (e instanceof LivingEntity livingEntity) + { + Brain brain = livingEntity.getBrain(); + Map, Optional>> memories = brain.getMemories(); + Optional> optmemory = memories.get(moduleType); + if (optmemory == null || !optmemory.isPresent()) + { + return Value.NULL; + } + ExpirableValue memory = optmemory.get(); + return ValueConversions.fromTimedMemory(e, memory.getTimeToLive(), memory.getValue()); + } + return Value.NULL; + }); + put("gamemode_id", (e, a) -> e instanceof ServerPlayer sp ? new NumericValue(sp.gameMode.getGameModeForPlayer().getId()) : Value.NULL); + put("permission_level", (e, a) -> { + if (e instanceof ServerPlayer spe) + { + for (int i = 4; i >= 0; i--) + { + if (spe.hasPermissions(i)) + { + return new NumericValue(i); + } + + } + return new NumericValue(0); + } + return Value.NULL; + }); + + put("player_type", (e, a) -> { + if (e instanceof Player p) + { + String moddedType = Carpet.isModdedPlayer(p); + if (moddedType != null) + { + return StringValue.of(moddedType); + } + MinecraftServer server = p.getCommandSenderWorld().getServer(); + if (server.isDedicatedServer()) + { + return new StringValue("multiplayer"); + } + boolean runningLan = server.isPublished(); + if (!runningLan) + { + return new StringValue("singleplayer"); + } + boolean isowner = server.isSingleplayerOwner(p.getGameProfile()); + if (isowner) + { + return new StringValue("lan_host"); + } + return new StringValue("lan player"); + // realms? + } + return Value.NULL; + }); + + put("client_brand", (e, a) -> e instanceof ServerPlayer sp ? StringValue.of(Carpet.getPlayerStatus(sp)) : Value.NULL); + put("team", (e, a) -> e.getTeam() == null ? Value.NULL : new StringValue(e.getTeam().getName())); + put("ping", (e, a) -> e instanceof ServerPlayer sp ? new NumericValue(sp.connection.latency()) : Value.NULL); + + //spectating_entity + // isGlowing + put("effect", (e, a) -> + { + if (!(e instanceof LivingEntity le)) + { + return Value.NULL; + } + if (a == null) + { + List effects = new ArrayList<>(); + for (MobEffectInstance p : le.getActiveEffects()) + { + effects.add(ListValue.of( + new StringValue(p.getDescriptionId().replaceFirst("^effect\\.minecraft\\.", "")), + new NumericValue(p.getAmplifier()), + new NumericValue(p.getDuration()) + )); + } + return ListValue.wrap(effects); + } + String effectName = a.getString(); + Holder potion = BuiltInRegistries.MOB_EFFECT.getHolder(ResourceKey.create(Registries.MOB_EFFECT, InputValidator.identifierOf(effectName))).orElseThrow(() -> new InternalExpressionException("No such an effect: " + effectName)); + if (!le.hasEffect(potion)) + { + return Value.NULL; + } + MobEffectInstance pe = le.getEffect(potion); + return ListValue.of(new NumericValue(pe.getAmplifier()), new NumericValue(pe.getDuration())); + }); + + put("health", (e, a) -> e instanceof LivingEntity le ? new NumericValue(le.getHealth()) : Value.NULL); + put("may_fly", (e, a) -> e instanceof ServerPlayer player ? BooleanValue.of(player.getAbilities().mayfly) : Value.NULL); + put("flying", (e, v) -> e instanceof ServerPlayer player ? BooleanValue.of(player.getAbilities().flying) : Value.NULL); + put("may_build", (e, v) -> e instanceof ServerPlayer player ? BooleanValue.of(player.getAbilities().mayBuild) : Value.NULL); + put("insta_build", (e, v) -> e instanceof ServerPlayer player ? BooleanValue.of(player.getAbilities().instabuild) : Value.NULL); + put("fly_speed", (e, v) -> e instanceof ServerPlayer player ? NumericValue.of(player.getAbilities().getFlyingSpeed()) : Value.NULL); + put("walk_speed", (e, v) -> e instanceof ServerPlayer player ? NumericValue.of(player.getAbilities().getWalkingSpeed()) : Value.NULL); + put("holds", (e, a) -> { + EquipmentSlot where = EquipmentSlot.MAINHAND; + if (a != null) + { + where = inventorySlots.get(a.getString()); + } + if (where == null) + { + throw new InternalExpressionException("Unknown inventory slot: " + a.getString()); + } + if (e instanceof LivingEntity le) + { + return ValueConversions.of(le.getItemBySlot(where), e.getServer().registryAccess()); + } + return Value.NULL; + }); + + put("selected_slot", (e, a) -> e instanceof Player p ? new NumericValue(p.getInventory().selected) : Value.NULL); + + put("active_block", (e, a) -> { + if (e instanceof ServerPlayer sp) + { + BlockPos pos = Vanilla.ServerPlayerGameMode_getCurrentBlockPosition(sp.gameMode); + if (pos == null) + { + return Value.NULL; + } + return new BlockValue(null, sp.serverLevel(), pos); + } + return Value.NULL; + }); + + put("breaking_progress", (e, a) -> { + if (e instanceof ServerPlayer sp) + { + int progress = Vanilla.ServerPlayerGameMode_getCurrentBlockBreakingProgress(sp.gameMode); + return progress < 0 ? Value.NULL : new NumericValue(progress); + } + return Value.NULL; + }); + + + put("facing", (e, a) -> { + int index = 0; + if (a != null) + { + index = (6 + (int) NumericValue.asNumber(a).getLong()) % 6; + } + if (index < 0 || index > 5) + { + throw new InternalExpressionException("Facing order should be between -6 and 5"); + } + + return new StringValue(Direction.orderedByNearest(e)[index].getSerializedName()); + }); + + put("trace", (e, a) -> + { + float reach = 4.5f; + boolean entities = true; + boolean liquids = false; + boolean blocks = true; + boolean exact = false; + + if (a != null) + { + if (!(a instanceof ListValue lv)) + { + reach = (float) NumericValue.asNumber(a).getDouble(); + } + else + { + List args = lv.getItems(); + if (args.size() == 0) + { + throw new InternalExpressionException("'trace' needs more arguments"); + } + reach = (float) NumericValue.asNumber(args.get(0)).getDouble(); + if (args.size() > 1) + { + entities = false; + blocks = false; + for (int i = 1; i < args.size(); i++) + { + String what = args.get(i).getString(); + if (what.equalsIgnoreCase("entities")) + { + entities = true; + } + else if (what.equalsIgnoreCase("blocks")) + { + blocks = true; + } + else if (what.equalsIgnoreCase("liquids")) + { + liquids = true; + } + else if (what.equalsIgnoreCase("exact")) + { + exact = true; + } + + else + { + throw new InternalExpressionException("Incorrect tracing: " + what); + } + } + } + } + } + else if (e instanceof ServerPlayer sp && sp.gameMode.isCreative()) + { + reach = 5.0f; + } + + HitResult hitres; + if (entities && !blocks) + { + hitres = Tracer.rayTraceEntities(e, 1, reach, reach * reach); + } + else if (entities) + { + hitres = Tracer.rayTrace(e, 1, reach, liquids); + } + else + { + hitres = Tracer.rayTraceBlocks(e, 1, reach, liquids); + } + + if (hitres == null) + { + return Value.NULL; + } + if (exact && hitres.getType() != HitResult.Type.MISS) + { + return ValueConversions.of(hitres.getLocation()); + } + switch (hitres.getType()) + { + case MISS: + return Value.NULL; + case BLOCK: + return new BlockValue((ServerLevel) e.getCommandSenderWorld(), ((BlockHitResult) hitres).getBlockPos()); + case ENTITY: + return new EntityValue(((EntityHitResult) hitres).getEntity()); + } + return Value.NULL; + }); + + put("attribute", (e, a) -> { + if (!(e instanceof LivingEntity el)) + { + return Value.NULL; + } + Registry attributes = e.level().registryAccess().registryOrThrow(Registries.ATTRIBUTE); + if (a == null) + { + AttributeMap container = el.getAttributes(); + return MapValue.wrap(attributes.holders().filter(container::hasAttribute).collect(Collectors.toMap(aa -> ValueConversions.of(aa.key()), aa -> NumericValue.of(container.getValue(aa))))); + } + ResourceLocation id = InputValidator.identifierOf(a.getString()); + Holder attrib = attributes.getHolder(id).orElseThrow( + () -> new InternalExpressionException("Unknown attribute: " + a.getString()) + ); + if (!el.getAttributes().hasAttribute(attrib)) + { + return Value.NULL; + } + return NumericValue.of(el.getAttributeValue(attrib)); + }); + + put("nbt", (e, a) -> { + CompoundTag nbttagcompound = e.saveWithoutId((new CompoundTag())); + if (a == null) + { + 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, @Nullable Value toWhat) + { + if (!(featureModifiers.containsKey(what))) + { + throw new InternalExpressionException("Unknown entity action: " + what); + } + try + { + featureModifiers.get(what).accept(getEntity(), toWhat); + } + catch (NullPointerException npe) + { + throw new InternalExpressionException("'modify' for '" + what + "' expects a value"); + } + catch (IndexOutOfBoundsException ind) + { + throw new InternalExpressionException("Wrong number of arguments for `modify` option: " + what); + } + } + + private static void updatePosition(Entity e, double x, double y, double z, float yaw, float pitch) + { + if ( + !Double.isFinite(x) || Double.isNaN(x) || + !Double.isFinite(y) || Double.isNaN(y) || + !Double.isFinite(z) || Double.isNaN(z) || + !Float.isFinite(yaw) || Float.isNaN(yaw) || + !Float.isFinite(pitch) || Float.isNaN(pitch) + ) + { + return; + } + if (e instanceof ServerPlayer sp) + { + // this forces position but doesn't angles for some reason. Need both in the API in the future. + EnumSet set = EnumSet.noneOf(RelativeMovement.class); + set.add(RelativeMovement.X_ROT); + set.add(RelativeMovement.Y_ROT); + sp.connection.teleport(x, y, z, yaw, pitch, set); + } + else + { + e.moveTo(x, y, z, yaw, pitch); + // we were sending to players for not-living entites, that were untracked. Living entities should be tracked. + //((ServerWorld) e.getEntityWorld()).getChunkManager().sendToNearbyPlayers(e, new EntityS2CPacket.(e)); + if (e instanceof LivingEntity le) + { + le.yBodyRotO = le.yRotO = yaw; + le.yHeadRotO = le.yHeadRot = yaw; + // seems universal for: + //e.setHeadYaw(yaw); + //e.setYaw(yaw); + } + else + { + ((ServerLevel) e.getCommandSenderWorld()).getChunkSource().broadcastAndSend(e, new ClientboundTeleportEntityPacket(e)); + } + } + } + + private static void updateVelocity(Entity e, double scale) + { + e.hurtMarked = true; + if (Math.abs(scale) > 10000) + { + CarpetScriptServer.LOG.warn("Moved entity " + e.getScoreboardName() + " " + e.getName() + " at " + e.position() + " extremely fast: " + e.getDeltaMovement()); + } + } + + private static final Map> featureModifiers = new HashMap>() + {{ + put("remove", (entity, value) -> entity.discard()); // using discard here - will see other options if valid + put("age", (e, v) -> e.tickCount = Math.abs((int) NumericValue.asNumber(v).getLong())); + put("health", (e, v) -> { + float health = (float) NumericValue.asNumber(v).getDouble(); + if (health <= 0f && e instanceof ServerPlayer player) + { + if (player.containerMenu != 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 le) + { + le.setHealth(health); + } + }); + + put("may_fly", (e, v) -> { + boolean mayFly = v.getBoolean(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().mayfly = mayFly; + if (!mayFly && player.getAbilities().flying) + { + player.getAbilities().flying = false; + } + player.onUpdateAbilities(); + } + }); + + put("flying", (e, v) -> { + boolean flying = v.getBoolean(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().flying = flying; + player.onUpdateAbilities(); + } + }); + + put("may_build", (e, v) -> { + boolean mayBuild = v.getBoolean(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().mayBuild = mayBuild; + player.onUpdateAbilities(); + } + }); + + put("insta_build", (e, v) -> { + boolean instaBuild = v.getBoolean(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().instabuild = instaBuild; + player.onUpdateAbilities(); + } + }); + + put("fly_speed", (e, v) -> { + float flySpeed = NumericValue.asNumber(v).getFloat(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().setFlyingSpeed(flySpeed); + player.onUpdateAbilities(); + } + }); + + put("walk_speed", (e, v) -> { + float walkSpeed = NumericValue.asNumber(v).getFloat(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().setWalkingSpeed(walkSpeed); + player.onUpdateAbilities(); + } + }); + + put("selected_slot", (e, v) -> + { + if (e instanceof ServerPlayer player) + { + int slot = NumericValue.asNumber(v).getInt(); + player.connection.send(new ClientboundSetCarriedItemPacket(slot)); + } + }); + + // todo add handling of the source for extra effects + /*put("damage", (e, v) -> { + float dmgPoints; + DamageSource source; + if (v instanceof final ListValue lv && lv.getItems().size() > 1) + { + List vals = lv.getItems(); + dmgPoints = (float) NumericValue.asNumber(v).getDouble(); + source = DamageSource ... yeah... + } + else + { + + } + });*/ + put("kill", (e, v) -> e.kill()); + put("location", (e, v) -> + { + if (!(v instanceof ListValue lv)) + { + throw new InternalExpressionException("Expected a list of 5 parameters as a second argument"); + } + List coords = lv.getItems(); + updatePosition(e, + NumericValue.asNumber(coords.get(0)).getDouble(), + NumericValue.asNumber(coords.get(1)).getDouble(), + NumericValue.asNumber(coords.get(2)).getDouble(), + (float) NumericValue.asNumber(coords.get(3)).getDouble(), + (float) NumericValue.asNumber(coords.get(4)).getDouble() + ); + }); + put("pos", (e, v) -> + { + if (!(v instanceof ListValue lv)) + { + throw new InternalExpressionException("Expected a list of 3 parameters as a second argument"); + } + List coords = lv.getItems(); + updatePosition(e, + NumericValue.asNumber(coords.get(0)).getDouble(), + NumericValue.asNumber(coords.get(1)).getDouble(), + NumericValue.asNumber(coords.get(2)).getDouble(), + e.getYRot(), + e.getXRot() + ); + }); + put("x", (e, v) -> updatePosition(e, NumericValue.asNumber(v).getDouble(), e.getY(), e.getZ(), e.getYRot(), e.getXRot())); + put("y", (e, v) -> updatePosition(e, e.getX(), NumericValue.asNumber(v).getDouble(), e.getZ(), e.getYRot(), e.getXRot())); + put("z", (e, v) -> updatePosition(e, e.getX(), e.getY(), NumericValue.asNumber(v).getDouble(), e.getYRot(), e.getXRot())); + put("yaw", (e, v) -> updatePosition(e, e.getX(), e.getY(), e.getZ(), ((float) NumericValue.asNumber(v).getDouble()) % 360, e.getXRot())); + put("head_yaw", (e, v) -> + { + if (e instanceof LivingEntity) + { + e.setYHeadRot((float) NumericValue.asNumber(v).getDouble() % 360); + } + }); + put("body_yaw", (e, v) -> + { + if (e instanceof LivingEntity) + { + e.setYRot((float) NumericValue.asNumber(v).getDouble() % 360); + } + }); + + put("pitch", (e, v) -> updatePosition(e, e.getX(), e.getY(), e.getZ(), e.getYRot(), Mth.clamp((float) NumericValue.asNumber(v).getDouble(), -90, 90))); + + put("look", (e, v) -> { + if (!(v instanceof ListValue lv)) + { + throw new InternalExpressionException("Expected a list of 3 parameters as a second argument"); + } + List vec = lv.getItems(); + float x = NumericValue.asNumber(vec.get(0)).getFloat(); + float y = NumericValue.asNumber(vec.get(1)).getFloat(); + float z = NumericValue.asNumber(vec.get(2)).getFloat(); + float l = Mth.sqrt(x * x + y * y + z * z); + if (l == 0) + { + return; + } + x /= l; + y /= l; + z /= l; + float pitch = (float) -Math.asin(y) / 0.017453292F; + float yaw = (float) (x == 0 && z == 0 ? e.getYRot() : Mth.atan2(-x, z) / 0.017453292F); + updatePosition(e, e.getX(), e.getY(), e.getZ(), yaw, pitch); + }); + + //"turn" + //"nod" + + put("move", (e, v) -> + { + if (!(v instanceof ListValue lv)) + { + throw new InternalExpressionException("Expected a list of 3 parameters as a second argument"); + } + List coords = lv.getItems(); + updatePosition(e, + e.getX() + NumericValue.asNumber(coords.get(0)).getDouble(), + e.getY() + NumericValue.asNumber(coords.get(1)).getDouble(), + e.getZ() + NumericValue.asNumber(coords.get(2)).getDouble(), + e.getYRot(), + e.getXRot() + ); + }); + + put("motion", (e, v) -> + { + if (!(v instanceof ListValue lv)) + { + throw new InternalExpressionException("Expected a list of 3 parameters as a second argument"); + } + List coords = lv.getItems(); + double dx = NumericValue.asNumber(coords.get(0)).getDouble(); + double dy = NumericValue.asNumber(coords.get(1)).getDouble(); + double dz = NumericValue.asNumber(coords.get(2)).getDouble(); + e.setDeltaMovement(dx, dy, dz); + updateVelocity(e, Mth.absMax(Mth.absMax(dx, dy), dz)); + }); + put("motion_x", (e, v) -> + { + Vec3 velocity = e.getDeltaMovement(); + double dv = NumericValue.asNumber(v).getDouble(); + e.setDeltaMovement(dv, velocity.y, velocity.z); + updateVelocity(e, dv); + }); + put("motion_y", (e, v) -> + { + Vec3 velocity = e.getDeltaMovement(); + double dv = NumericValue.asNumber(v).getDouble(); + e.setDeltaMovement(velocity.x, dv, velocity.z); + updateVelocity(e, dv); + }); + put("motion_z", (e, v) -> + { + Vec3 velocity = e.getDeltaMovement(); + double dv = NumericValue.asNumber(v).getDouble(); + e.setDeltaMovement(velocity.x, velocity.y, dv); + updateVelocity(e, dv); + }); + + put("accelerate", (e, v) -> + { + if (!(v instanceof ListValue lv)) + { + throw new InternalExpressionException("Expected a list of 3 parameters as a second argument"); + } + List coords = lv.getItems(); + e.push( + NumericValue.asNumber(coords.get(0)).getDouble(), + NumericValue.asNumber(coords.get(1)).getDouble(), + NumericValue.asNumber(coords.get(2)).getDouble() + ); + updateVelocity(e, e.getDeltaMovement().length()); + + }); + put("custom_name", (e, v) -> { + if (v.isNull()) + { + e.setCustomNameVisible(false); + e.setCustomName(null); + return; + } + boolean showName = false; + if (v instanceof ListValue lv) + { + showName = lv.getItems().get(1).getBoolean(); + v = lv.getItems().get(0); + } + e.setCustomNameVisible(showName); + e.setCustomName(FormattedTextValue.getTextByValue(v)); + }); + + put("persistence", (e, v) -> + { + if (!(e instanceof Mob mob)) + { + return; + } + if (v == null) + { + v = Value.TRUE; + } + Vanilla.Mob_setPersistence(mob, v.getBoolean()); + }); + + put("dismount", (e, v) -> e.stopRiding()); + put("mount", (e, v) -> { + if (v instanceof EntityValue ev) + { + e.startRiding(ev.getEntity(), true); + } + if (e instanceof ServerPlayer sp) + { + sp.connection.send(new ClientboundSetPassengersPacket(e)); + } + }); + put("unmountable", (e, v) -> Vanilla.Entity_setPermanentVehicle(e, v == null || v.getBoolean())); + put("drop_passengers", (e, v) -> e.ejectPassengers()); + put("mount_passengers", (e, v) -> { + if (v == null) + { + throw new InternalExpressionException("'mount_passengers' needs entities to ride"); + } + if (v instanceof EntityValue ev) + { + ev.getEntity().startRiding(e); + } + else if (v instanceof ListValue lv) + { + for (Value element : lv.getItems()) + { + if (element instanceof EntityValue ev) + { + ev.getEntity().startRiding(e); + } + } + } + }); + put("tag", (e, v) -> { + if (v == null) + { + throw new InternalExpressionException("'tag' requires parameters"); + } + if (v instanceof ListValue lv) + { + for (Value element : lv.getItems()) + { + e.addTag(element.getString()); + } + } + else + { + e.addTag(v.getString()); + } + }); + put("clear_tag", (e, v) -> { + if (v == null) + { + throw new InternalExpressionException("'clear_tag' requires parameters"); + } + if (v instanceof ListValue lv) + { + for (Value element : lv.getItems()) + { + e.removeTag(element.getString()); + } + } + else + { + e.removeTag(v.getString()); + } + }); + //put("target", (e, v) -> { + // // attacks indefinitely - might need to do it through tasks + // if (e instanceof MobEntity) + // { + // LivingEntity elb = assertEntityArgType(LivingEntity.class, v); + // ((MobEntity) e).setTarget(elb); + // } + //}); + put("breeding_age", (e, v) -> + { + if (e instanceof AgeableMob am) + { + am.setAge((int) NumericValue.asNumber(v).getLong()); + } + }); + put("talk", (e, v) -> { + // attacks indefinitely + if (e instanceof Mob mob) + { + mob.playAmbientSound(); + } + }); + put("home", (e, v) -> { + if (!(e instanceof PathfinderMob ec)) + { + return; + } + if (v == null) + { + throw new InternalExpressionException("'home' requires at least one position argument, and optional distance, or null to cancel"); + } + if (v.isNull()) + { + ec.restrictTo(BlockPos.ZERO, -1); + Map tasks = Vanilla.Mob_getTemporaryTasks(ec); + Vanilla.Mob_getAI(ec, false).removeGoal(tasks.get("home")); + tasks.remove("home"); + return; + } + + BlockPos pos; + int distance = 16; + + if (v instanceof BlockValue bv) + { + pos = bv.getPos(); + if (pos == null) + { + throw new InternalExpressionException("Block is not positioned in the world"); + } + } + else if (v instanceof ListValue lv) + { + List list = lv.getItems(); + Vector3Argument locator = Vector3Argument.findIn(list, 0, false, false); + pos = BlockPos.containing(locator.vec.x, locator.vec.y, locator.vec.z); + if (list.size() > locator.offset) + { + distance = (int) NumericValue.asNumber(list.get(locator.offset)).getLong(); + } + } + else + { + throw new InternalExpressionException("'home' requires at least one position argument, and optional distance"); + } + + ec.restrictTo(pos, distance); + Map tasks = Vanilla.Mob_getTemporaryTasks(ec); + if (!tasks.containsKey("home")) + { + Goal task = new MoveTowardsRestrictionGoal(ec, 1.0D); + tasks.put("home", task); + Vanilla.Mob_getAI(ec, false).addGoal(10, task); + } + }); //requires mixing + + put("spawn_point", (e, a) -> { + if (!(e instanceof ServerPlayer spe)) + { + return; + } + if (a == null) + { + spe.setRespawnPosition(null, null, 0, false, false); + } + else if (a instanceof ListValue lv) + { + List params = lv.getItems(); + Vector3Argument blockLocator = Vector3Argument.findIn(params, 0, false, false); + BlockPos pos = BlockPos.containing(blockLocator.vec); + ResourceKey world = spe.getCommandSenderWorld().dimension(); + float angle = spe.getYHeadRot(); + boolean forced = false; + if (params.size() > blockLocator.offset) + { + Value worldValue = params.get(blockLocator.offset); + world = ValueConversions.dimFromValue(worldValue, spe.getServer()).dimension(); + if (params.size() > blockLocator.offset + 1) + { + angle = NumericValue.asNumber(params.get(blockLocator.offset + 1), "angle").getFloat(); + if (params.size() > blockLocator.offset + 2) + { + forced = params.get(blockLocator.offset + 2).getBoolean(); + } + } + } + spe.setRespawnPosition(world, pos, angle, forced, false); + } + else if (a instanceof BlockValue bv) + { + if (bv.getPos() == null || bv.getWorld() == null) + { + throw new InternalExpressionException("block for spawn modification should be localised in the world"); + } + spe.setRespawnPosition(bv.getWorld().dimension(), bv.getPos(), e.getYRot(), true, false); // yaw + } + else if (a.isNull()) + { + spe.setRespawnPosition(null, null, 0, false, false); + } + else + { + throw new InternalExpressionException("modifying player respawn point requires a block position, optional world, optional angle, and optional force"); + } + }); + + put("pickup_delay", (e, v) -> + { + if (e instanceof ItemEntity ie) + { + ie.setPickUpDelay((int) NumericValue.asNumber(v).getLong()); + } + }); + + put("despawn_timer", (e, v) -> + { + if (e instanceof LivingEntity le) + { + le.setNoActionTime((int) NumericValue.asNumber(v).getLong()); + } + }); + + put("portal_cooldown", (e, v) -> + { + if (v == null) + { + throw new InternalExpressionException("'portal_cooldown' requires a value to set"); + } + Vanilla.Entity_setPublicNetherPortalCooldown(e, NumericValue.asNumber(v).getInt()); + }); + + put("portal_timer", (e, v) -> + { + if (v == null) + { + throw new InternalExpressionException("'portal_timer' requires a value to set"); + } + Vanilla.Entity_setPortalTimer(e, NumericValue.asNumber(v).getInt()); + }); + + put("ai", (e, v) -> + { + if (e instanceof Mob mob) + { + mob.setNoAi(!v.getBoolean()); + } + }); + + put("no_clip", (e, v) -> + { + if (v == null) + { + e.noPhysics = true; + } + else + { + e.noPhysics = v.getBoolean(); + } + }); + put("effect", (e, v) -> + { + if (!(e instanceof LivingEntity le)) + { + return; + } + if (v == null) + { + le.removeAllEffects(); + return; + } + else if (v instanceof ListValue lv) + { + List list = lv.getItems(); + if (list.size() >= 1 && list.size() <= 6) + { + String effectName = list.get(0).getString(); + Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(InputValidator.identifierOf(effectName)).orElseThrow(() -> new InternalExpressionException("No such an effect: " + effectName)); + if (list.size() == 1) + { + le.removeEffect(effect); + return; + } + int duration = (int) NumericValue.asNumber(list.get(1)).getLong(); + if (duration == 0) + { + le.removeEffect(effect); + return; + } + if (duration < 0) + { + duration = -1; + } + int amplifier = 0; + if (list.size() > 2) + { + amplifier = (int) NumericValue.asNumber(list.get(2)).getLong(); + } + boolean showParticles = true; + if (list.size() > 3) + { + showParticles = list.get(3).getBoolean(); + } + boolean showIcon = true; + if (list.size() > 4) + { + showIcon = list.get(4).getBoolean(); + } + boolean ambient = false; + if (list.size() > 5) + { + ambient = list.get(5).getBoolean(); + } + le.addEffect(new MobEffectInstance(effect, duration, amplifier, ambient, showParticles, showIcon)); + return; + } + } + else + { + String effectName = v.getString(); + Holder effect = BuiltInRegistries.MOB_EFFECT.getHolder(InputValidator.identifierOf(effectName)).orElseThrow(() -> new InternalExpressionException("No such an effect: " + effectName)); + le.removeEffect(effect); + return; + } + throw new InternalExpressionException("'effect' needs either no arguments (clear) or effect name, duration, and optional amplifier, show particles, show icon and ambient"); + }); + + put("gamemode", (e, v) -> { + if (!(e instanceof ServerPlayer sp)) + { + return; + } + GameType toSet = v instanceof NumericValue ? + GameType.byId(((NumericValue) v).getInt()) : + GameType.byName(v.getString().toLowerCase(Locale.ROOT), null); + if (toSet != null) + { + sp.setGameMode(toSet); + } + }); + + put("jumping", (e, v) -> { + if (!(e instanceof LivingEntity le)) + { + return; + } + le.setJumping(v.getBoolean()); + }); + + put("jump", (e, v) -> { + if (e instanceof LivingEntity le) + { + Vanilla.LivingEntity_setJumping(le); + } + else + { + EntityTools.genericJump(e); + } + }); + + put("swing", (e, v) -> { + if (e instanceof LivingEntity le) + { + InteractionHand hand = InteractionHand.MAIN_HAND; + if (v != null) + { + String handString = v.getString().toLowerCase(Locale.ROOT); + if (handString.equals("offhand") || handString.equals("off_hand")) + { + hand = InteractionHand.OFF_HAND; + } + } + le.swing(hand, true); + } + }); + + put("silent", (e, v) -> e.setSilent(v.getBoolean())); + + put("gravity", (e, v) -> e.setNoGravity(!v.getBoolean())); + + put("invulnerable", (e, v) -> { + boolean invulnerable = v.getBoolean(); + if (e instanceof ServerPlayer player) + { + player.getAbilities().invulnerable = invulnerable; + player.onUpdateAbilities(); + } + else + { + e.setInvulnerable(invulnerable); + } + }); + + put("fire", (e, v) -> e.setRemainingFireTicks((int) NumericValue.asNumber(v).getLong())); + put("frost", (e, v) -> e.setTicksFrozen((int) NumericValue.asNumber(v).getLong())); + + put("hunger", (e, v) -> { + if (e instanceof Player p) + { + p.getFoodData().setFoodLevel((int) NumericValue.asNumber(v).getLong()); + } + }); + + put("exhaustion", (e, v) -> { + if (e instanceof Player p) + { + p.getFoodData().setExhaustion(NumericValue.asNumber(v).getFloat()); + } + }); + + put("add_exhaustion", (e, v) -> { + if (e instanceof Player p) + { + p.getFoodData().addExhaustion(NumericValue.asNumber(v).getFloat()); + } + }); + + put("absorption", (e, v) -> { + if (e instanceof Player p) + { + p.setAbsorptionAmount(NumericValue.asNumber(v, "absorbtion").getFloat()); + } + }); + + put("add_xp", (e, v) -> { + if (e instanceof Player p) + { + p.giveExperiencePoints(NumericValue.asNumber(v, "add_xp").getInt()); + } + }); + + put("xp_level", (e, v) -> { + if (e instanceof Player p) + { + p.giveExperienceLevels(NumericValue.asNumber(v, "xp_level").getInt() - p.experienceLevel); + } + }); + + put("xp_progress", (e, v) -> { + if (e instanceof ServerPlayer p) + { + p.experienceProgress = NumericValue.asNumber(v, "xp_progress").getFloat(); + p.connection.send(new ClientboundSetExperiencePacket(p.experienceProgress, p.totalExperience, p.experienceLevel)); + } + }); + + put("xp_score", (e, v) -> { + if (e instanceof Player p) + { + p.setScore(NumericValue.asNumber(v, "xp_score").getInt()); + } + }); + + put("saturation", (e, v) -> { + if (e instanceof Player p) + { + p.getFoodData().setSaturation(NumericValue.asNumber(v, "saturation").getFloat()); + } + }); + + put("air", (e, v) -> e.setAirSupply(NumericValue.asNumber(v, "air").getInt())); + + put("breaking_progress", (e, a) -> { + if (e instanceof ServerPlayer sp) + { + int progress = (a == null || a.isNull()) ? -1 : NumericValue.asNumber(a).getInt(); + Vanilla.ServerPlayerGameMode_setBlockBreakingProgress(sp.gameMode, progress); + } + }); + + put("nbt", (e, v) -> { + if (!(e instanceof Player)) + { + UUID uUID = e.getUUID(); + Value tagValue = NBTSerializableValue.fromValue(v); + if (tagValue instanceof NBTSerializableValue nbtsv) + { + e.load(nbtsv.getCompoundTag()); + e.setUUID(uUID); + } + } + }); + put("nbt_merge", (e, v) -> { + if (!(e instanceof Player)) + { + UUID uUID = e.getUUID(); + Value tagValue = NBTSerializableValue.fromValue(v); + if (tagValue instanceof NBTSerializableValue nbtsv) + { + CompoundTag nbttagcompound = e.saveWithoutId((new CompoundTag())); + nbttagcompound.merge(nbtsv.getCompoundTag()); + e.load(nbttagcompound); + e.setUUID(uUID); + } + } + }); + + put("blue_skull", (e, v) -> { + if (e instanceof WitherSkull w) + { + w.setDangerous(v.getBoolean()); + } + + }); + put("offering_flower", (e, v) -> { + if (e instanceof IronGolem ig) + { + ig.offerFlower(v.getBoolean()); + } + + }); + put("item", (e, v) -> { + ItemStack item = ValueConversions.getItemStackFromValue(v, true, e.level().registryAccess()); + if (e instanceof ItemEntity itementity) + { + itementity.setItem(item); + } + if (e instanceof ItemFrame itemframe) + { + itemframe.setItem(item); + } + }); + // "dimension" [] + // "count", [] + // "effect_"name [] + }}; + + public void setEvent(CarpetContext cc, String eventName, FunctionValue fun, List args) + { + EntityEventsGroup.Event event = EntityEventsGroup.Event.byName.get(eventName); + if (event == null) + { + throw new InternalExpressionException("Unknown entity event: " + eventName); + } + Vanilla.Entity_getEventContainer(getEntity()).addEvent(event, cc.host, fun, args); + } + + @Override + public net.minecraft.nbt.Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + CompoundTag tag = new CompoundTag(); + tag.put("Data", getEntity().saveWithoutId(new CompoundTag())); + Registry> reg = getEntity().level().registryAccess().registryOrThrow(Registries.ENTITY_TYPE); + tag.put("Name", StringTag.valueOf(reg.getKey(getEntity().getType()).toString())); + return tag; + } +} 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 0000000..b08e094 --- /dev/null +++ b/src/main/java/carpet/script/value/FormattedTextValue.java @@ -0,0 +1,113 @@ +package carpet.script.value; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class FormattedTextValue extends StringValue +{ + Component text; + + public FormattedTextValue(Component text) + { + super(null); + this.text = text; + } + + public static Value combine(Value left, Value right) + { + MutableComponent text; + if (left instanceof FormattedTextValue ftv) + { + text = ftv.getText().copy(); + } + else + { + if (left.isNull()) + { + return right; + } + text = Component.literal(left.getString()); + } + + if (right instanceof FormattedTextValue ftv) + { + text.append(ftv.getText().copy()); + return new FormattedTextValue(text); + } + if (right.isNull()) + { + return left; + } + text.append(right.getString()); + return new FormattedTextValue(text); + } + + public static Value of(Component text) + { + return text == null ? Value.NULL : new FormattedTextValue(text); + } + + @Override + public String getString() + { + return text.getString(); + } + + @Override + public boolean getBoolean() + { + return !text.getString().isEmpty(); + } + + @Override + public Value clone() + { + return new FormattedTextValue(text); + } + + @Override + public String getTypeString() + { + return "text"; + } + + public Component getText() + { + return text; + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + return StringTag.valueOf(Component.Serializer.toJson(text, regs)); + } + + @Override + public Value add(Value o) + { + return combine(this, o); + } + + public String serialize(RegistryAccess regs) + { + return Component.Serializer.toJson(text, regs); + } + + public static FormattedTextValue deserialize(String serialized, RegistryAccess regs) + { + return new FormattedTextValue(Component.Serializer.fromJson(serialized, regs)); + } + + public static Component getTextByValue(Value value) + { + return (value instanceof FormattedTextValue ftv) ? ftv.getText() : Component.literal(value.getString()); + } + +} diff --git a/src/main/java/carpet/script/value/FrameworkValue.java b/src/main/java/carpet/script/value/FrameworkValue.java new file mode 100644 index 0000000..a7fc5cd --- /dev/null +++ b/src/main/java/carpet/script/value/FrameworkValue.java @@ -0,0 +1,37 @@ +package carpet.script.value; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.Tag; + +public abstract class FrameworkValue extends Value +{ + @Override + public String getString() + { + throw new UnsupportedOperationException("Scarpet language component cannot be used"); + } + + @Override + public boolean getBoolean() + { + throw new UnsupportedOperationException("Scarpet language component cannot be used"); + } + + @Override + public Value clone() + { + throw new UnsupportedOperationException("Scarpet language component cannot be used"); + } + + @Override + public int hashCode() + { + throw new UnsupportedOperationException("Scarpet language component cannot be used as map key"); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + throw new UnsupportedOperationException("Scarpet language component cannot be serialized to the tag"); + } +} diff --git a/src/main/java/carpet/script/value/FunctionAnnotationValue.java b/src/main/java/carpet/script/value/FunctionAnnotationValue.java new file mode 100644 index 0000000..308ae87 --- /dev/null +++ b/src/main/java/carpet/script/value/FunctionAnnotationValue.java @@ -0,0 +1,49 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.Tag; + +public class FunctionAnnotationValue extends Value +{ + public enum Type + { + GLOBAL, VARARG + } + + public Type type; + + public FunctionAnnotationValue(Value variable, Type type) + { + if (variable.boundVariable == null) + { + throw new InternalExpressionException("You can only borrow variables from the outer scope"); + } + this.boundVariable = variable.boundVariable; + this.type = type; + } + + @Override + public String getString() + { + return boundVariable; + } + + @Override + public boolean getBoolean() + { + return false; + } + + @Override + public int hashCode() + { + throw new UnsupportedOperationException("Global value cannot be used as a map key"); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + throw new UnsupportedOperationException("Global value cannot be serialized to the tag"); + } +} diff --git a/src/main/java/carpet/script/value/FunctionSignatureValue.java b/src/main/java/carpet/script/value/FunctionSignatureValue.java new file mode 100644 index 0000000..e1d9fd1 --- /dev/null +++ b/src/main/java/carpet/script/value/FunctionSignatureValue.java @@ -0,0 +1,29 @@ +package carpet.script.value; + +import java.util.List; + +public class FunctionSignatureValue extends FrameworkValue +{ + private final String identifier; + private final List arguments; + private final List globals; + private final String varArgs; + + public FunctionSignatureValue(String name, List args, String varArgs, List globals) + { + this.identifier = name; + this.arguments = args; + this.varArgs = varArgs; + this.globals = globals; + } + public String identifier() + { + return identifier; + } + public List arguments() + { + return arguments; + } + public List globals() {return globals;} + public String varArgs() { return varArgs;} +} diff --git a/src/main/java/carpet/script/value/FunctionUnpackedArgumentsValue.java b/src/main/java/carpet/script/value/FunctionUnpackedArgumentsValue.java new file mode 100644 index 0000000..d44563c --- /dev/null +++ b/src/main/java/carpet/script/value/FunctionUnpackedArgumentsValue.java @@ -0,0 +1,23 @@ +package carpet.script.value; + +import java.util.List; + +public class FunctionUnpackedArgumentsValue extends ListValue +{ + public FunctionUnpackedArgumentsValue(List list) + { + super(list); + } + + @Override + public Value clone() + { + return new FunctionUnpackedArgumentsValue(items); + } + + @Override + public Value deepcopy() + { + return new FunctionUnpackedArgumentsValue(((ListValue) super.deepcopy()).items); + } +} diff --git a/src/main/java/carpet/script/value/FunctionValue.java b/src/main/java/carpet/script/value/FunctionValue.java new file mode 100644 index 0000000..1cc7f44 --- /dev/null +++ b/src/main/java/carpet/script/value/FunctionValue.java @@ -0,0 +1,340 @@ +package carpet.script.value; + +import carpet.script.CarpetScriptServer; +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Fluff; +import carpet.script.LazyValue; +import carpet.script.Module; +import carpet.script.Tokenizer; +import carpet.script.exception.BreakStatement; +import carpet.script.exception.ContinueStatement; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ReturnStatement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +import javax.annotation.Nullable; + +public class FunctionValue extends Value implements Fluff.ILazyFunction +{ + private final Expression expression; + private final Tokenizer.Token token; + private final String name; + private final LazyValue body; + private Map outerState; + private final List args; + private final String varArgs; + private static long variantCounter = 1; + private long variant; + + private FunctionValue(Expression expression, Tokenizer.Token token, String name, LazyValue body, List args, String varArgs) + { + this.expression = expression; + this.token = token; + this.name = name; + this.body = body; + this.args = args; + this.varArgs = varArgs; + this.outerState = null; + variant = 0L; + } + + public FunctionValue(Expression expression, Tokenizer.Token token, String name, LazyValue body, List args, String varArgs, Map outerState) + { + this.expression = expression; + this.token = token; + this.name = name; + this.body = body; + this.args = args; + this.varArgs = varArgs; + this.outerState = outerState; + variant = variantCounter++; + } + + @Override + public String getString() + { + return name; + } + + public Module getModule() + { + return expression.module; + } + + @Override + public String getPrettyString() + { + List stringArgs = new ArrayList<>(args); + if (outerState != null) + { + stringArgs.addAll(outerState.entrySet().stream().map(e -> + "outer(" + e.getKey() + ") = " + e.getValue().evalValue(null).getPrettyString()).toList()); + } + return (name.equals("_") ? "" : name) + "(" + String.join(", ", stringArgs) + ")"; + } + + public String fullName() + { + return (name.equals("_") ? "" : name) + (expression.module == null ? "" : "[" + expression.module.name() + "]"); + } + + @Override + public boolean getBoolean() + { + return true; + } + + @Override + protected Value clone() + { + FunctionValue ret = new FunctionValue(expression, token, name, body, args, varArgs); + ret.outerState = this.outerState; + ret.variant = this.variant; + return ret; + } + + @Override + public int hashCode() + { + return name.hashCode() + (int) variant; + } + + @Override + public boolean equals(Object o) + { + return o instanceof FunctionValue fv && name.equals(fv.name) && variant == fv.variant; + } + + @Override + public int compareTo(Value o) + { + if (o instanceof FunctionValue fv) + { + int nameSame = this.name.compareTo(fv.name); + return nameSame != 0 ? nameSame : (int) (variant - fv.variant); + } + return getString().compareTo(o.getString()); + } + + @Override + public double readDoubleNumber() + { + return getNumParams(); + } + + @Override + public String getTypeString() + { + return "function"; + } + + @Override + public Value slice(long from, Long to) + { + throw new InternalExpressionException("Cannot slice a function"); + } + + @Override + public int getNumParams() + { + return args.size(); + } + + @Override + public boolean numParamsVaries() + { + return varArgs != null; + } + + public LazyValue callInContext(Context c, Context.Type type, List params) + { + try + { + return execute(c, type, expression, token, params, null); + } + catch (ExpressionException exc) + { + exc.stack.add(this); + throw exc; + } + catch (InternalExpressionException exc) + { + exc.stack.add(this); + throw new ExpressionException(c, expression, token, exc.getMessage(), exc.stack); + } + catch (ArithmeticException exc) + { + throw new ExpressionException(c, expression, token, "Your math is wrong, " + exc.getMessage(), Collections.singletonList(this)); + } + } + + public void checkArgs(int candidates) + { + int actual = getArguments().size(); + + if (candidates < actual) + { + throw new InternalExpressionException("Function " + getPrettyString() + " requires at least " + actual + " arguments"); + } + if (candidates > actual && getVarArgs() == null) + { + throw new InternalExpressionException("Function " + getPrettyString() + " requires " + actual + " arguments"); + } + } + + public static List unpackArgs(List lazyParams, Context c) + { + // TODO we shoudn't need that if all fuctions are not lazy really + List params = new ArrayList<>(); + for (LazyValue lv : lazyParams) + { + Value param = lv.evalValue(c, Context.NONE); + if (param instanceof FunctionUnpackedArgumentsValue) + { + CarpetScriptServer.LOG.error("How did we get here?"); + params.addAll(((ListValue) param).getItems()); + } + else + { + params.add(param); + } + } + return params; + } + + @Override + public LazyValue lazyEval(Context c, Context.Type type, Expression e, Tokenizer.Token t, List lazyParams) + { + List resolvedParams = unpackArgs(lazyParams, c); + return execute(c, type, e, t, resolvedParams, null); + } + + public LazyValue execute(Context c, Context.Type type, Expression e, Tokenizer.Token t, List params, @Nullable ThreadValue freshNewCallingThread) + { + assertArgsOk(params, fixedArgs -> { + if (fixedArgs) // wrong number of args for fixed args + { + throw new ExpressionException(c, e, t, + "Incorrect number of arguments for function " + name + + ". Should be " + args.size() + ", not " + params.size() + " like " + args + ); + } + // too few args for varargs + + List argList = new ArrayList<>(args); + argList.add("... " + varArgs); + throw new ExpressionException(c, e, t, + "Incorrect number of arguments for function " + name + + ". Should be at least " + args.size() + ", not " + params.size() + " like " + argList + ); + }); + Context newFrame = c.recreate(); + if (freshNewCallingThread != null) + { + newFrame.setThreadContext(freshNewCallingThread); + } + + if (outerState != null) + { + outerState.forEach(newFrame::setVariable); + } + for (int i = 0; i < args.size(); i++) + { + String arg = args.get(i); + Value val = params.get(i).reboundedTo(arg); // todo check if we need to copy that + newFrame.setVariable(arg, (cc, tt) -> val); + } + if (varArgs != null) + { + List extraParams = new ArrayList<>(); + for (int i = args.size(), mx = params.size(); i < mx; i++) + { + extraParams.add(params.get(i).reboundedTo(null)); // copy by value I guess + } + Value rest = ListValue.wrap(extraParams).bindTo(varArgs); // didn't we just copied that? + newFrame.setVariable(varArgs, (cc, tt) -> rest); + + } + Value retVal; + try + { + retVal = body.evalValue(newFrame, type); // todo not sure if we need to propagete type / consider boolean context in defined functions - answer seems ye + } + catch (BreakStatement | ContinueStatement exc) + { + throw new ExpressionException(c, e, t, "'continue' and 'break' can only be called inside loop function bodies"); + } + catch (ReturnStatement returnStatement) + { + retVal = returnStatement.retval; + } + Value otherRetVal = retVal; + return (cc, tt) -> otherRetVal; + } + + public Expression getExpression() + { + return expression; + } + + public Tokenizer.Token getToken() + { + return token; + } + + public List getArguments() + { + return args; + } + + public String getVarArgs() + { + return varArgs; + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + return StringTag.valueOf(getString()); + } + + public void assertArgsOk(List list, Consumer feedback) + { + int size = list.size(); + if (varArgs == null && args.size() != size) // wrong number of args for fixed args + { + feedback.accept(true); + } + else if (varArgs != null && args.size() > size) // too few args for varargs + { + feedback.accept(false); + } + } + + @Override + public boolean pure() + { + return false; + } + + @Override + public boolean transitive() + { + return false; + } +} diff --git a/src/main/java/carpet/script/value/LContainerValue.java b/src/main/java/carpet/script/value/LContainerValue.java new file mode 100644 index 0000000..e6b8bee --- /dev/null +++ b/src/main/java/carpet/script/value/LContainerValue.java @@ -0,0 +1,24 @@ +package carpet.script.value; + +public class LContainerValue extends FrameworkValue +{ + private final ContainerValueInterface container; + private final Value address; + public static final LContainerValue NULL_CONTAINER = new LContainerValue(null, null); + + public LContainerValue(ContainerValueInterface c, Value v) + { + container = c; + address = v; + } + + public ContainerValueInterface container() + { + return container; + } + + public Value address() + { + return address; + } +} diff --git a/src/main/java/carpet/script/value/LazyListValue.java b/src/main/java/carpet/script/value/LazyListValue.java new file mode 100644 index 0000000..99ae419 --- /dev/null +++ b/src/main/java/carpet/script/value/LazyListValue.java @@ -0,0 +1,250 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +public abstract class LazyListValue extends AbstractListValue implements Iterator +{ + public static LazyListValue rangeDouble(double from, double to, double step) + { + return new LazyListValue() + { + { + if (step == 0) + { + throw new InternalExpressionException("Range will never end with a zero step"); + } + this.start = from; + this.current = this.start; + this.limit = to; + this.stepp = step; + } + + private final double start; + private double current; + private final double limit; + private final double stepp; + + @Override + public Value next() + { + Value val = new NumericValue(current); + current += stepp; + return val; + } + + @Override + public void reset() + { + current = start; + } + + @Override + public boolean hasNext() + { + return stepp > 0 ? (current < limit) : (current > limit); + } + + @Override + public String getString() + { + return String.format(Locale.ROOT, "[%s, %s, ..., %s)", NumericValue.of(start).getString(), NumericValue.of(start + stepp).getString(), NumericValue.of(limit).getString()); + } + }; + } + + public static LazyListValue rangeLong(long from, long to, long step) + { + return new LazyListValue() + { + { + if (step == 0) + { + throw new InternalExpressionException("Range will never end with a zero step"); + } + this.start = from; + this.current = this.start; + this.limit = to; + this.stepp = step; + } + + private final long start; + private long current; + private final long limit; + private final long stepp; + + @Override + public Value next() + { + Value val = new NumericValue(current); + current += stepp; + return val; + } + + @Override + public void reset() + { + current = start; + } + + @Override + public boolean hasNext() + { + return stepp > 0 ? (current < limit) : (current > limit); + } + + @Override + public String getString() + { + return String.format(Locale.ROOT, "[%s, %s, ..., %s)", NumericValue.of(start).getString(), NumericValue.of(start + stepp).getString(), NumericValue.of(limit).getString()); + } + }; + } + + @Override + public String getString() + { + return "[...]"; + } + + @Override + public boolean getBoolean() + { + return hasNext(); + } + + @Override + public void fatality() + { + reset(); + } + + public abstract void reset(); + + @Override + public Iterator iterator() + { + return this; + } + + public List unroll() + { + List result = new ArrayList<>(); + this.forEachRemaining(v -> { + if (v != Value.EOL) + { + result.add(v); + } + }); + fatality(); + return result; + } + + @Override + public Value slice(long from, Long to) + { + if (to == null || to < 0) + { + to = (long) Integer.MAX_VALUE; + } + if (from < 0) + { + from = 0; + } + if (from > to) + { + return ListValue.of(); + } + List result = new ArrayList<>(); + int i; + for (i = 0; i < from; i++) + { + if (hasNext()) + { + next(); + } + else + { + fatality(); + return ListValue.wrap(result); + } + } + for (i = (int) from; i < to; i++) + { + if (hasNext()) + { + result.add(next()); + } + else + { + fatality(); + return ListValue.wrap(result); + } + } + return ListValue.wrap(result); + } + + @Override + public Value add(Value other) + { + throw new InternalExpressionException("Cannot add to iterators"); + } + + @Override + public boolean equals(Object o) + { + return false; + } + + @Override + public String getTypeString() + { + return "iterator"; + } + + @Override + public Object clone() + { + Object copy; + try + { + copy = super.clone(); + } + catch (CloneNotSupportedException e) + { + throw new InternalExpressionException("Cannot copy iterators"); + } + ((LazyListValue) copy).reset(); + return copy; + } + + @Override + public Value fromConstant() + { + return (Value) clone(); + } + + @Override + public int hashCode() + { + return ("i" + getString()).hashCode(); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + return StringTag.valueOf(getString()); + } +} diff --git a/src/main/java/carpet/script/value/ListValue.java b/src/main/java/carpet/script/value/ListValue.java new file mode 100644 index 0000000..dfea225 --- /dev/null +++ b/src/main/java/carpet/script/value/ListValue.java @@ -0,0 +1,610 @@ +package carpet.script.value; + +import carpet.script.LazyValue; +import carpet.script.exception.InternalExpressionException; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +import static java.lang.Math.abs; + +public class ListValue extends AbstractListValue implements ContainerValueInterface +{ + protected final List items; + + @Override + public String getString() + { + return "[" + items.stream().map(Value::getString).collect(Collectors.joining(", ")) + "]"; + } + + @Override + public String getPrettyString() + { + return items.size() < 8 + ? "[" + items.stream().map(Value::getPrettyString).collect(Collectors.joining(", ")) + "]" + : "[" + items.get(0).getPrettyString() + ", " + items.get(1).getPrettyString() + ", ..., " + items.get(items.size() - 2).getPrettyString() + ", " + items.get(items.size() - 1).getPrettyString() + "]"; + } + + @Override + public boolean getBoolean() + { + return !items.isEmpty(); + } + + @Override + public Value clone() + { + return new ListValue(items); + } + + @Override + public Value deepcopy() + { + List copyItems = new ArrayList<>(items.size()); + for (Value entry : items) + { + copyItems.add(entry.deepcopy()); + } + return new ListValue(copyItems); + } + + public ListValue(Collection list) + { + items = new ArrayList<>(list); + } + + protected ListValue(List list) + { + items = list; + } + + 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(Stream stream) + { + return wrap(stream.collect(Collectors.toList())); + } + + public static ListValue wrap(List list) + { + return new ListValue(list); + } + + public static ListValue of(Value... list) + { + return new ListValue(new ArrayList<>(Arrays.asList(list))); + } + + public static ListValue ofNums(Number... list) + { + List valList = new ArrayList<>(); + for (Number i : list) + { + valList.add(new NumericValue(i.doubleValue())); + } + return new ListValue(valList); + } + + public static LazyValue lazyEmpty() + { + Value ret = new ListValue(); + return (c, t) -> ret; + } + + private ListValue() + { + items = new ArrayList<>(); + } + + @Override + public Value add(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue list) + { + List otherItems = list.items; + if (otherItems.size() == items.size()) + { + for (int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).add(otherItems.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot add two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.add(other)); + } + } + return output; + } + + @Override + public void append(Value v) + { + items.add(v); + } + + @Override + public Value subtract(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue list) + { + List otherItems = list.items; + if (otherItems.size() == items.size()) + { + for (int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).subtract(otherItems.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot subtract two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.subtract(other)); + } + } + return output; + } + + public void subtractFrom(Value v) // if I ever do -= then it wouod remove items + { + throw new UnsupportedOperationException(); // TODO + } + + + @Override + public Value multiply(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue list) + { + List otherItems = list.items; + if (otherItems.size() == items.size()) + { + for (int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).multiply(otherItems.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot multiply two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.multiply(other)); + } + } + return output; + } + + @Override + public Value divide(Value other) + { + ListValue output = new ListValue(); + if (other instanceof ListValue list) + { + List otherItems = list.items; + if (otherItems.size() == items.size()) + { + for (int i = 0, size = items.size(); i < size; i++) + { + output.items.add(items.get(i).divide(otherItems.get(i))); + } + } + else + { + throw new InternalExpressionException("Cannot divide two lists of uneven sizes"); + } + } + else + { + for (Value v : items) + { + output.items.add(v.divide(other)); + } + } + return output; + } + + @Override + public int compareTo(Value o) + { + if (o instanceof ListValue ol) + { + int size = this.getItems().size(); + int otherSize = ol.getItems().size(); + if (size != otherSize) + { + return size - otherSize; + } + if (size == 0) + { + return 0; + } + for (int i = 0; i < size; i++) + { + int res = this.items.get(i).compareTo(ol.items.get(i)); + if (res != 0) + { + return res; + } + } + return 0; + } + return getString().compareTo(o.getString()); + } + + @Override + public boolean equals(Object o) + { + return o instanceof ListValue list && getItems().equals(list.getItems()); + } + + public List getItems() + { + return items; + } + + @Override + public Iterator iterator() + { + return new ArrayList<>(items).iterator(); + } // should be thread safe + + @Override + public List unpack() + { + return new ArrayList<>(items); + } + + public void extend(List subList) + { + items.addAll(subList); + } + + /** + * Finds a proper list index >=0 and < len that correspont to the rolling index value of idx + * + * @param idx + * @param len + * @return + */ + public static int normalizeIndex(long idx, int len) + { + if (idx >= 0 && idx < len) + { + return (int) idx; + } + long range = abs(idx) / len; + idx += (range + 2) * len; + idx = idx % len; + return (int) idx; + } + + public static class ListConstructorValue extends ListValue + { + public ListConstructorValue(Collection list) + { + super(list); + } + } + + @Override + public int length() + { + return items.size(); + } + + @Override + public Value in(Value value1) + { + for (int i = 0; i < items.size(); i++) + { + Value v = items.get(i); + if (v.equals(value1)) + { + return new NumericValue(i); + } + } + return Value.NULL; + } + + @Override + public Value slice(long fromDesc, Long toDesc) + { + List items = getItems(); + int size = items.size(); + int from = normalizeIndex(fromDesc, size); + if (toDesc == null) + { + return new ListValue(new ArrayList<>(getItems().subList(from, size))); + } + int to = normalizeIndex(toDesc, size + 1); + if (from > to) + { + return ListValue.of(); + } + return new ListValue(new ArrayList<>(getItems().subList(from, to))); + } + + @Override + public Value split(Value delimiter) + { + ListValue result = new ListValue(); + if (delimiter == null) + { + this.forEach(item -> result.items.add(of(item))); + return result; + } + int startIndex = 0; + int index = 0; + for (Value val : this.items) + { + index++; + if (val.equals(delimiter)) + { + result.items.add(new ListValue(new ArrayList<>(this.items.subList(startIndex, index - 1)))); + startIndex = index; + } + } + result.items.add(new ListValue(new ArrayList<>(this.items.subList(startIndex, length())))); + return result; + } + + @Override + public double readDoubleNumber() + { + return items.size(); + } + + @Override + public boolean put(Value where, Value value, Value conditionValue) + { + String condition = conditionValue.getString(); + if (condition.equalsIgnoreCase("insert")) + { + return put(where, value, false, false); + } + if (condition.equalsIgnoreCase("extend")) + { + return put(where, value, false, true); + } + if (condition.equalsIgnoreCase("replace")) + { + return put(where, value, true, false); + } + throw new InternalExpressionException("List 'put' modifier could be either 'insert', 'replace', or extend"); + } + + @Override + public boolean put(Value ind, Value value) + { + return put(ind, value, true, false); + } + + private boolean put(Value ind, Value value, boolean replace, boolean extend) + { + if (ind.isNull()) + { + if (extend && value instanceof AbstractListValue) + { + ((AbstractListValue) value).iterator().forEachRemaining(items::add); + } + else + { + items.add(value); + } + } + else + { + int numitems = items.size(); + if (!(ind instanceof NumericValue)) + { + return false; + } + int index = (int) ((NumericValue) ind).getLong(); + if (index < 0) + {// only for values < 0 + index = normalizeIndex(index, numitems); + } + if (replace) + { + while (index >= items.size()) + { + items.add(Value.NULL); + } + items.set(index, value); + return true; + } + while (index > items.size()) + { + items.add(Value.NULL); + } + + if (extend && value instanceof AbstractListValue) + { + Iterable iterable = ((AbstractListValue) value)::iterator; + List appendix = StreamSupport.stream(iterable.spliterator(), false).collect(Collectors.toList()); + items.addAll(index, appendix); + return true; + } + items.add(index, value); + } + return true; + } + + @Override + public Value get(Value value) + { + int size = items.size(); + return size == 0 ? Value.NULL : items.get(normalizeIndex(NumericValue.asNumber(value, "'address' to a list index").getLong(), size)); + } + + @Override + public boolean has(Value where) + { + long index = NumericValue.asNumber(where, "'address' to a list index").getLong(); + return index >= 0 && index < items.size(); + } + + @Override + public boolean delete(Value where) + { + if (!(where instanceof NumericValue) || items.isEmpty()) + { + return false; + } + long index = ((NumericValue) where).getLong(); + items.remove(normalizeIndex(index, items.size())); + return true; + } + + @Override + public String getTypeString() + { + return "list"; + } + + @Override + public int hashCode() + { + return items.hashCode(); + } + + private enum TagTypeCompat + { + INT, + LONG, + DBL, + LIST, + MAP, + STRING; + + private static TagTypeCompat getType(Tag tag) + { + if (tag instanceof IntTag) + { + return INT; + } + if (tag instanceof LongTag) + { + return LONG; + } + if (tag instanceof DoubleTag) + { + return DBL; + } + if (tag instanceof ListTag) + { + return LIST; + } + if (tag instanceof CompoundTag) + { + return MAP; + } + return STRING; + } + } + + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + int argSize = items.size(); + if (argSize == 0) + { + return new ListTag(); + } + ListTag tag = new ListTag(); + if (argSize == 1) + { + tag.add(items.get(0).toTag(force, regs)); + return tag; + } + // figuring out the types + List tags = new ArrayList<>(); + items.forEach(v -> tags.add(v.toTag(force, regs))); + Set cases = EnumSet.noneOf(TagTypeCompat.class); + tags.forEach(t -> cases.add(TagTypeCompat.getType(t))); + if (cases.size() == 1) // well, one type of items + { + tag.addAll(tags); + return tag; + } + if (cases.contains(TagTypeCompat.LIST) + || cases.contains(TagTypeCompat.MAP) + || cases.contains(TagTypeCompat.STRING)) // incompatible types + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + tags.forEach(t -> tag.add(StringTag.valueOf(t.getAsString()))); + return tag; + } + // only numbers / mixed types + tags.forEach(cases.contains(TagTypeCompat.DBL) + ? (t -> tag.add(DoubleTag.valueOf(((NumericTag) t).getAsDouble()))) + : (t -> tag.add(LongTag.valueOf(((NumericTag) t).getAsLong())))); + return tag; + } + + @Override + public JsonElement toJson() + { + JsonArray array = new JsonArray(); + for (Value el : items) + { + array.add(el.toJson()); + } + return array; + } +} diff --git a/src/main/java/carpet/script/value/MapValue.java b/src/main/java/carpet/script/value/MapValue.java new file mode 100644 index 0000000..ba4b167 --- /dev/null +++ b/src/main/java/carpet/script/value/MapValue.java @@ -0,0 +1,276 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.Collections; +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; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; + +public class MapValue extends AbstractListValue implements ContainerValueInterface +{ + private final Map map; + + private MapValue() + { + map = new HashMap<>(); + } + + public MapValue(List kvPairs) + { + this(); + kvPairs.forEach(this::put); + } + + public MapValue(Set keySet) + { + this(); + keySet.forEach(v -> map.put(v, Value.NULL)); + } + + @Override + public Iterator iterator() + { + return new ArrayList<>(map.keySet()).iterator(); + } + + @Override + public List unpack() + { + return map.entrySet().stream().map(e -> ListValue.of(e.getKey(), e.getValue())).collect(Collectors.toList()); + } + + @Override + public String getString() + { + return "{" + map.entrySet().stream().map(p -> p.getKey().getString() + ": " + p.getValue().getString()).collect(Collectors.joining(", ")) + "}"; + } + + @Override + public String getPrettyString() + { + if (map.size() < 6) + { + return "{" + map.entrySet().stream().map(p -> p.getKey().getPrettyString() + ": " + p.getValue().getPrettyString()).collect(Collectors.joining(", ")) + "}"; + } + List keys = new ArrayList<>(map.keySet()); + int max = keys.size(); + return "{" + keys.get(0).getPrettyString() + ": " + map.get(keys.get(0)).getPrettyString() + ", " + + keys.get(1).getPrettyString() + ": " + map.get(keys.get(1)).getPrettyString() + ", ..., " + + keys.get(max - 2).getPrettyString() + ": " + map.get(keys.get(max - 2)).getPrettyString() + ", " + + keys.get(max - 1).getPrettyString() + ": " + map.get(keys.get(max - 1)).getPrettyString() + "}"; + } + + @Override + public boolean getBoolean() + { + return !map.isEmpty(); + } + + @Override + public Value clone() + { + return new MapValue(map); + } + + @Override + public Value deepcopy() + { + Map copyMap = new HashMap<>(); + map.forEach((key, value) -> copyMap.put(key.deepcopy(), value.deepcopy())); + return new MapValue(copyMap); + } + + private MapValue(Map other) + { + map = other; + } + + public static MapValue wrap(Map other) + { + return new MapValue(other); + } + + @Override + public Value add(Value o) + { + Map newItems = new HashMap<>(map); + if (o instanceof MapValue mapValue) + { + newItems.putAll(mapValue.map); + } + else if (o instanceof AbstractListValue alv) + { + for (Value value : alv) + { + newItems.put(value, Value.NULL); + } + } + else + { + newItems.put(o, Value.NULL); + } + return MapValue.wrap(newItems); + } + + @Override + public Value subtract(Value v) + { + throw new InternalExpressionException("Cannot subtract from a map value"); + } + + @Override + public Value multiply(Value v) + { + throw new InternalExpressionException("Cannot multiply with a map value"); + } + + @Override + public Value divide(Value v) + { + throw new InternalExpressionException("Cannot divide a map value"); + } + + public void put(Value v) + { + if (!(v instanceof ListValue pair)) + { + map.put(v, Value.NULL); + return; + } + if (pair.getItems().size() != 2) + { + throw new InternalExpressionException("Map constructor requires elements that have two items"); + } + map.put(pair.getItems().get(0), pair.getItems().get(1)); + } + + @Override + public void append(Value v) + { + map.put(v, Value.NULL); + } + + @Override + public int compareTo(Value o) + { + throw new InternalExpressionException("Cannot compare with a map value"); + } + + @Override + public boolean equals(Object o) + { + return o instanceof MapValue mapValue && map.equals(mapValue.map); + } + + public Map getMap() + { + return map; + } + + public void extend(List subList) + { + subList.forEach(this::put); + } + + @Override + public int length() + { + return map.size(); + } + + @Override + public Value in(Value value) + { + return map.containsKey(value) ? value : Value.NULL; + } + + @Override + public Value slice(long from, Long to) + { + throw new InternalExpressionException("Cannot slice a map value"); + } + + @Override + public Value split(Value delimiter) + { + throw new InternalExpressionException("Cannot split a map value"); + } + + @Override + public double readDoubleNumber() + { + return map.size(); + } + + @Override + public Value get(Value v2) + { + return map.getOrDefault(v2, Value.NULL); + } + + @Override + public boolean has(Value where) + { + return map.containsKey(where); + } + + @Override + public boolean delete(Value where) + { + return map.remove(where) != null; + } + + @Override + public boolean put(Value key, Value value) + { + return map.put(key, value) != null; + } + + @Override + public String getTypeString() + { + return "map"; + } + + @Override + public int hashCode() + { + return map.hashCode(); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + CompoundTag tag = new CompoundTag(); + map.forEach((k, v) -> + { + if (!force && !(k instanceof StringValue)) + { + throw new NBTSerializableValue.IncompatibleTypeException(k); + } + tag.put(k.getString(), v.toTag(force, regs)); + }); + return tag; + } + + @Override + public JsonElement toJson() + { + JsonObject jsonMap = new JsonObject(); + List keys = new ArrayList<>(map.keySet()); + Collections.sort(keys); + keys.forEach(k -> jsonMap.add(k.getString(), map.get(k).toJson())); + return jsonMap; + } +} diff --git a/src/main/java/carpet/script/value/NBTSerializableValue.java b/src/main/java/carpet/script/value/NBTSerializableValue.java new file mode 100644 index 0000000..f6df234 --- /dev/null +++ b/src/main/java/carpet/script/value/NBTSerializableValue.java @@ -0,0 +1,786 @@ +package carpet.script.value; + +import carpet.script.CarpetContext; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.external.Vanilla; +import carpet.script.utils.EquipmentInventory; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import java.util.ArrayList; +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; +import java.util.regex.Pattern; + +import net.minecraft.commands.arguments.NbtPathArgument; +import net.minecraft.commands.arguments.item.ItemInput; +import net.minecraft.commands.arguments.item.ItemParser; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.particles.ItemParticleOption; +import net.minecraft.nbt.CollectionTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.EndTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import net.minecraft.nbt.TagParser; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainerHolder; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.npc.InventoryCarrier; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +import javax.annotation.Nullable; + +public class NBTSerializableValue extends Value implements ContainerValueInterface +{ + private String nbtString = null; + private Tag nbtTag = null; + private Supplier nbtSupplier = null; + private boolean owned = false; + + private NBTSerializableValue() + { + } + + public NBTSerializableValue(String nbtString) + { + nbtSupplier = () -> + { + try + { + return (new TagParser(new StringReader(nbtString))).readValue(); + } + catch (CommandSyntaxException e) + { + throw new InternalExpressionException("Incorrect NBT data: " + nbtString); + } + }; + owned = true; + } + + public NBTSerializableValue(Tag tag) + { + nbtTag = tag; + owned = true; + } + + public static Value of(Tag tag) + { + if (tag == null) + { + return Value.NULL; + } + return new NBTSerializableValue(tag); + } + + public NBTSerializableValue(Supplier tagSupplier) + { + nbtSupplier = tagSupplier; + } + + public static Value fromStack(ItemStack stack, RegistryAccess regs) + { + NBTSerializableValue value = new NBTSerializableValue(); + value.nbtSupplier = () -> stack.saveOptional(regs); + return value; + } + + public static Value nameFromRegistryId(@Nullable ResourceLocation id) + { + return StringValue.of(nameFromResource(id)); + } + + @Nullable + public static String nameFromResource(@Nullable ResourceLocation id) + { + return id == null ? null : id.getNamespace().equals("minecraft") ? id.getPath() : id.toString(); + } + + @Nullable + public static NBTSerializableValue parseString(String nbtString) + { + try + { + Tag tag = (new TagParser(new StringReader(nbtString))).readValue(); + NBTSerializableValue value = new NBTSerializableValue(tag); + value.nbtString = null; + return value; + } + catch (CommandSyntaxException e) + { + return null; + } + } + + public static NBTSerializableValue parseStringOrFail(String nbtString) + { + NBTSerializableValue result = parseString(nbtString); + if (result == null) + { + throw new InternalExpressionException("Incorrect NBT tag: " + nbtString); + } + return result; + } + + + @Override + public Value clone() + { + // sets only nbttag, even if emtpy; + NBTSerializableValue copy = new NBTSerializableValue(nbtTag); + copy.nbtSupplier = this.nbtSupplier; + copy.nbtString = this.nbtString; + copy.owned = this.owned; + return copy; + } + + @Override + public Value deepcopy() + { + NBTSerializableValue copy = (NBTSerializableValue) clone(); + copy.owned = false; + ensureOwnership(); + return copy; + } + + @Override + public Value fromConstant() + { + return deepcopy(); + } + + // stolen from HopperBlockEntity, adjusted for threaded operation + public static Container getInventoryAt(ServerLevel world, BlockPos blockPos) + { + Container inventory = null; + BlockState blockState = world.getBlockState(blockPos); + Block block = blockState.getBlock(); + if (block instanceof final WorldlyContainerHolder containerHolder) + { + inventory = containerHolder.getContainer(blockState, world, blockPos); + } + else if (blockState.hasBlockEntity()) + { + BlockEntity blockEntity = BlockValue.getBlockEntity(world, blockPos); + if (blockEntity instanceof final Container inventoryHolder) + { + inventory = inventoryHolder; + if (inventory instanceof ChestBlockEntity && block instanceof final ChestBlock chestBlock) + { + inventory = ChestBlock.getContainer(chestBlock, blockState, world, blockPos, true); + } + } + } + + if (inventory == null) + { + List list = world.getEntities( + (Entity) null, //TODO check this matches the correct method + new AABB( + blockPos.getX() - 0.5D, blockPos.getY() - 0.5D, blockPos.getZ() - 0.5D, + blockPos.getX() + 0.5D, blockPos.getY() + 0.5D, blockPos.getZ() + 0.5D), + EntitySelector.CONTAINER_ENTITY_SELECTOR + ); + if (!list.isEmpty()) + { + inventory = (Container) list.get(world.random.nextInt(list.size())); + } + } + + return inventory; + } + + public static InventoryLocator locateInventory(CarpetContext c, List params, int offset) + { + try + { + Value v1 = params.get(offset); + if (v1.isNull()) + { + offset++; + v1 = params.get(offset); + } + else if (v1 instanceof StringValue) + { + String strVal = v1.getString().toLowerCase(Locale.ROOT); + if (strVal.equals("enderchest")) + { + Value v2 = params.get(1 + offset); + ServerPlayer player = EntityValue.getPlayerByValue(c.server(), v2); + if (player == null) + { + throw new InternalExpressionException("enderchest inventory requires player argument"); + } + return new InventoryLocator(player, player.blockPosition(), player.getEnderChestInventory(), offset + 2, true); + } + if (strVal.equals("equipment")) + { + Value v2 = params.get(1 + offset); + if (!(v2 instanceof final EntityValue ev)) + { + throw new InternalExpressionException("Equipment inventory requires a living entity argument"); + } + Entity e = ev.getEntity(); + if (!(e instanceof final LivingEntity le)) + { + throw new InternalExpressionException("Equipment inventory requires a living entity argument"); + } + return new InventoryLocator(e, e.blockPosition(), new EquipmentInventory(le), offset + 2); + } + boolean isEnder = strVal.startsWith("enderchest_"); + if (isEnder) + { + strVal = strVal.substring(11); // len("enderchest_") + } + ServerPlayer player = c.server().getPlayerList().getPlayerByName(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.blockPosition(), + isEnder ? player.getEnderChestInventory() : player.getInventory(), + offset + 1, + isEnder + ); + } + if (v1 instanceof final EntityValue ev) + { + Container inv = null; + Entity e = ev.getEntity(); + if (e instanceof final Player pe) + { + inv = pe.getInventory(); + } + else if (e instanceof final Container container) + { + inv = container; + } + else if (e instanceof final InventoryCarrier io) + { + inv = io.getInventory(); + } + else if (e instanceof final AbstractHorse ibi) + { + inv = Vanilla.AbstractHorse_getInventory(ibi); // horse only + } + else if (e instanceof final LivingEntity le) + { + return new InventoryLocator(e, e.blockPosition(), new EquipmentInventory(le), offset + 1); + } + return inv == null ? null : new InventoryLocator(e, e.blockPosition(), inv, offset + 1); + + } + if (v1 instanceof final BlockValue bv) + { + BlockPos pos = bv.getPos(); + if (pos == null) + { + throw new InternalExpressionException("Block to access inventory needs to be positioned in the world"); + } + Container inv = getInventoryAt(c.level(), pos); + return inv == null ? null : new InventoryLocator(pos, pos, inv, offset + 1); + } + if (v1 instanceof final ListValue lv) + { + List args = lv.getItems(); + BlockPos pos = BlockPos.containing( + NumericValue.asNumber(args.get(0)).getDouble(), + NumericValue.asNumber(args.get(1)).getDouble(), + NumericValue.asNumber(args.get(2)).getDouble()); + Container inv = getInventoryAt(c.level(), pos); + return inv == null ? null : new InventoryLocator(pos, pos, inv, offset + 1); + } + if (v1 instanceof final ScreenValue screenValue) + { + return !screenValue.isOpen() ? null : new InventoryLocator(screenValue.getPlayer(), screenValue.getPlayer().blockPosition(), screenValue.getInventory(), offset + 1); + } + BlockPos pos = BlockPos.containing( + NumericValue.asNumber(v1).getDouble(), + NumericValue.asNumber(params.get(1 + offset)).getDouble(), + NumericValue.asNumber(params.get(2 + offset)).getDouble()); + Container inv = getInventoryAt(c.level(), pos); + return inv == null ? null : new InventoryLocator(pos, pos, inv, offset + 3); + } + catch (IndexOutOfBoundsException e) + { + throw new InternalExpressionException("Inventory should be defined either by three coordinates, a block value, an entity, or a screen"); + } + } + + private static final Map itemCache = new HashMap<>(); + + public static ItemStack parseItem(String itemString, RegistryAccess regs) + { + return parseItem(itemString, null, regs); + } + + public static ItemStack parseItem(String itemString, @Nullable CompoundTag customTag, RegistryAccess regs) + { + if (customTag != null) { + return ItemStack.parseOptional(regs, customTag); + } + try + { + ItemInput res = itemCache.get(itemString); // [SCARY SHIT] persistent caches over server reloads + if (res != null) + { + return res.createItemStack(1, false); + } + ItemParser.ItemResult parser = (new ItemParser(regs)).parse(new StringReader(itemString)); + res = new ItemInput(parser.item(), parser.components()); + + itemCache.put(itemString, res); + if (itemCache.size() > 64000) + { + itemCache.clear(); + } + return res.createItemStack(1, false); + } + catch (CommandSyntaxException e) + { + throw new ThrowStatement(itemString, Throwables.UNKNOWN_ITEM); + } + } + + public static int validateSlot(int slot, Container inv) + { + int invSize = inv.getContainerSize(); + if (slot < 0) + { + slot = invSize + slot; + } + return slot < 0 || slot >= invSize ? inv.getContainerSize() : slot; // outside of inventory + } + + private static Value decodeSimpleTag(Tag t) + { + if (t instanceof final NumericTag number) + { + // short and byte will never exceed float's precision, even int won't + return t instanceof LongTag || t instanceof IntTag ? NumericValue.of(number.getAsLong()) : NumericValue.of(number.getAsNumber()); + } + if (t instanceof StringTag) + { + return StringValue.of(t.getAsString()); + } + if (t instanceof EndTag) + { + return Value.NULL; + } + throw new InternalExpressionException("How did we get here: Unknown nbt element class: " + t.getType().getName()); + } + + private static Value decodeTag(Tag t) + { + return t instanceof CompoundTag || t instanceof CollectionTag ? new NBTSerializableValue(() -> t) : decodeSimpleTag(t); + } + + private static Value decodeTagDeep(Tag t) + { + if (t instanceof final CompoundTag ctag) + { + Map pairs = new HashMap<>(); + for (String key : ctag.getAllKeys()) + { + pairs.put(new StringValue(key), decodeTagDeep(ctag.get(key))); + } + return MapValue.wrap(pairs); + } + if (t instanceof final CollectionTag ltag) + { + List elems = new ArrayList<>(); + for (Tag elem : ltag) + { + elems.add(decodeTagDeep(elem)); + } + return ListValue.wrap(elems); + } + return decodeSimpleTag(t); + } + + public Value toValue() + { + return decodeTagDeep(this.getTag()); + } + + public static Value fromValue(Value v) + { + if (v instanceof NBTSerializableValue) + { + return v; + } + if (v.isNull()) + { + return Value.NULL; + } + return NBTSerializableValue.parseStringOrFail(v.getString()); + } + + public Tag getTag() + { + if (nbtTag == null) + { + nbtTag = nbtSupplier.get(); + } + return nbtTag; + } + + @Override + public boolean equals(Object o) + { + return o instanceof final NBTSerializableValue nbtsv ? getTag().equals(nbtsv.getTag()) : super.equals(o); + } + + @Override + public String getString() + { + if (nbtString == null) + { + nbtString = getTag().toString(); + } + return nbtString; + } + + @Override + public boolean getBoolean() + { + Tag tag = getTag(); + if (tag instanceof final CompoundTag ctag) + { + return !(ctag).isEmpty(); + } + if (tag instanceof final CollectionTag ltag) + { + return !(ltag).isEmpty(); + } + if (tag instanceof final NumericTag number) + { + return number.getAsDouble() != 0.0; + } + if (tag instanceof StringTag) + { + return !tag.getAsString().isEmpty(); + } + return true; + } + + public CompoundTag getCompoundTag() + { + try + { + ensureOwnership(); + return (CompoundTag) getTag(); + } + catch (ClassCastException e) + { + throw new InternalExpressionException(getString() + " is not a valid compound tag"); + } + } + + @Override + public boolean put(Value where, Value value) + { + return put(where, value, new StringValue("replace")); + } + + @Override + public boolean put(Value where, Value value, Value conditions) + { + /// WIP + ensureOwnership(); + NbtPathArgument.NbtPath path = cachePath(where.getString()); + Tag tagToInsert = value instanceof final NBTSerializableValue nbtsv + ? nbtsv.getTag() + : new NBTSerializableValue(value.getString()).getTag(); + boolean modifiedTag; + if (conditions instanceof final NumericValue number) + { + modifiedTag = modifyInsert((int) number.getLong(), path, tagToInsert); + } + else + { + String ops = conditions.getString(); + if (ops.equalsIgnoreCase("merge")) + { + modifiedTag = modifyMerge(path, tagToInsert); + } + else if (ops.equalsIgnoreCase("replace")) + { + modifiedTag = modifyReplace(path, tagToInsert); + } + else + { + return false; + } + } + if (modifiedTag) + { + dirty(); + } + return modifiedTag; + } + + + private boolean modifyInsert(int index, NbtPathArgument.NbtPath nbtPath, Tag newElement) + { + return modifyInsert(index, nbtPath, newElement, this.getTag()); + } + + private boolean modifyInsert(int index, NbtPathArgument.NbtPath nbtPath, Tag newElement, Tag currentTag) + { + Collection targets; + try + { + targets = nbtPath.getOrCreate(currentTag, ListTag::new); + } + catch (CommandSyntaxException e) + { + return false; + } + + boolean modified = false; + for (Tag target : targets) + { + if (!(target instanceof final CollectionTag targetList)) + { + continue; + } + try + { + if (!targetList.addTag(index < 0 ? targetList.size() + index + 1 : index, newElement.copy())) + { + return false; + } + modified = true; + } + catch (IndexOutOfBoundsException ignored) + { + } + } + return modified; + } + + + private boolean modifyMerge(NbtPathArgument.NbtPath nbtPath, Tag replacement) //nbtPathArgumentType$NbtPath_1, list_1) + { + if (!(replacement instanceof final CompoundTag replacementCompound)) + { + return false; + } + Tag ownTag = getTag(); + try + { + for (Tag target : nbtPath.getOrCreate(ownTag, CompoundTag::new)) + { + if (!(target instanceof final CompoundTag targetCompound)) + { + continue; + } + targetCompound.merge(replacementCompound); + } + } + catch (CommandSyntaxException ignored) + { + return false; + } + return true; + } + + private boolean modifyReplace(NbtPathArgument.NbtPath nbtPath, Tag replacement) //nbtPathArgumentType$NbtPath_1, list_1) + { + Tag tag = getTag(); + String pathText = nbtPath.toString(); + if (pathText.endsWith("]")) // workaround for array replacement or item in the array replacement + { + if (nbtPath.remove(tag) == 0) + { + return false; + } + Pattern pattern = Pattern.compile("\\[[^\\[]*]$"); + Matcher matcher = pattern.matcher(pathText); + if (!matcher.find()) // malformed path + { + return false; + } + String arrAccess = matcher.group(); + int pos; + if (arrAccess.length() == 2) // we just removed entire array + { + pos = 0; + } + else + { + try + { + pos = Integer.parseInt(arrAccess.substring(1, arrAccess.length() - 1)); + } + catch (NumberFormatException e) + { + return false; + } + } + NbtPathArgument.NbtPath newPath = cachePath(pathText.substring(0, pathText.length() - arrAccess.length())); + return modifyInsert(pos, newPath, replacement, tag); + } + try + { + nbtPath.set(tag, replacement); + } + catch (CommandSyntaxException e) + { + return false; + } + return true; + } + + @Override + public Value get(Value value) + { + String valString = value.getString(); + NbtPathArgument.NbtPath path = cachePath(valString); + try + { + List tags = path.get(getTag()); + if (tags.isEmpty()) + { + return Value.NULL; + } + if (tags.size() == 1 && !valString.endsWith("[]")) + { + return NBTSerializableValue.decodeTag(tags.get(0)); + } + return ListValue.wrap(tags.stream().map(NBTSerializableValue::decodeTag)); + } + catch (CommandSyntaxException ignored) + { + } + return Value.NULL; + } + + @Override + public boolean has(Value where) + { + return cachePath(where.getString()).countMatching(getTag()) > 0; + } + + private void ensureOwnership() + { + if (!owned) + { + nbtTag = getTag().copy(); + nbtString = null; + nbtSupplier = null; // just to be sure + owned = true; + } + } + + private void dirty() + { + nbtString = null; + } + + @Override + public boolean delete(Value where) + { + NbtPathArgument.NbtPath path = cachePath(where.getString()); + ensureOwnership(); + int removed = path.remove(getTag()); + if (removed > 0) + { + dirty(); + return true; + } + return false; + } + + public record InventoryLocator(Object owner, BlockPos position, Container inventory, int offset, + boolean isEnder) + { + InventoryLocator(Object owner, BlockPos pos, Container i, int o) + { + this(owner, pos, i, o, false); + } + } + + private static final Map pathCache = new HashMap<>(); + + private static NbtPathArgument.NbtPath cachePath(String arg) + { + NbtPathArgument.NbtPath res = pathCache.get(arg); + if (res != null) + { + return res; + } + try + { + res = NbtPathArgument.nbtPath().parse(new StringReader(arg)); + } + catch (CommandSyntaxException exc) + { + throw new InternalExpressionException("Incorrect nbt path: " + arg); + } + if (pathCache.size() > 1024) + { + pathCache.clear(); + } + pathCache.put(arg, res); + return res; + } + + @Override + public String getTypeString() + { + return "nbt"; + } + + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + ensureOwnership(); + return getTag(); + } + + public static class IncompatibleTypeException extends RuntimeException + { + public final Value val; + + public IncompatibleTypeException(Value val) + { + this.val = val; + } + } + +} diff --git a/src/main/java/carpet/script/value/NullValue.java b/src/main/java/carpet/script/value/NullValue.java new file mode 100644 index 0000000..4828982 --- /dev/null +++ b/src/main/java/carpet/script/value/NullValue.java @@ -0,0 +1,119 @@ +package carpet.script.value; + +import java.util.ArrayList; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; + +public class NullValue extends NumericValue // TODO check nonsingleton code +{ + public static final NullValue NULL = new NullValue(); + + @Override + public String getString() + { + return "null"; + } + + @Override + public String getPrettyString() + { + return "null"; + } + + @Override + public boolean getBoolean() + { + return false; + } + + @Override + public Value clone() + { + return new NullValue(); + } + + protected NullValue() + { + super(0); + } + + @Override + public boolean equals(Object o) + { + return o instanceof Value value && value.isNull(); + } + + @Override + public Value slice(long fromDesc, Long toDesc) + { + return Value.NULL; + } + + @Override + public NumericValue opposite() + { + return Value.NULL; + } + + @Override + public int length() + { + return 0; + } + + @Override + public int compareTo(Value o) + { + return o.isNull() ? 0 : -1; + } + + @Override + public Value in(Value value) + { + return Value.NULL; + } + + @Override + public String getTypeString() + { + return "null"; + } + + @Override + public int hashCode() + { + return 0; + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + return StringTag.valueOf("null"); + } + + @Override + public Value split(Value delimiter) + { + return ListValue.wrap(new ArrayList<>()); + } + + @Override + public JsonElement toJson() + { + return JsonNull.INSTANCE; + } + + @Override + public boolean isNull() + { + return true; + } +} diff --git a/src/main/java/carpet/script/value/NumericValue.java b/src/main/java/carpet/script/value/NumericValue.java new file mode 100644 index 0000000..a3df310 --- /dev/null +++ b/src/main/java/carpet/script/value/NumericValue.java @@ -0,0 +1,342 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.RegistryAccess; +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.Locale; + +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.LongTag; +import net.minecraft.nbt.Tag; + +import static java.lang.Math.abs; +import static java.lang.Math.signum; + +public class NumericValue extends Value +{ + private final double value; + private Long longValue; + private static final double epsilon = abs(32 * ((7 * 0.1) * 10 - 7)); + private static final MathContext displayRounding = new MathContext(12, RoundingMode.HALF_EVEN); + + public static NumericValue asNumber(Value v1, String id) + { + if (v1 instanceof NumericValue nv) + { + return nv; + } + throw new InternalExpressionException("Argument " + id + " has to be of a numeric type"); + } + + public static NumericValue asNumber(Value v1) + { + if (v1 instanceof NumericValue nv) + { + return nv; + } + throw new InternalExpressionException("Operand has to be of a numeric type"); + } + + public static Value of(T value) + { + if (value == null) + { + return Value.NULL; + } + if (value.doubleValue() == value.longValue()) + { + return new NumericValue(value.longValue()); + } + if (value instanceof Float) + { + return new NumericValue(0.000_001D * Math.round(1_000_000.0D * value.doubleValue())); + } + return new NumericValue(value.doubleValue()); + } + + + @Override + public String getString() + { + if (longValue != null) + { + return Long.toString(getLong()); + } + try + { + if (Double.isInfinite(value)) + { + return (value > 0) ? "INFINITY" : "-INFINITY"; + } + if (Double.isNaN(value)) + { + return "NaN"; + } + if (abs(value) < epsilon) + { + return (signum(value) < 0) ? "-0" : "0"; //zero rounding fails with big decimals + } + // dobules have 16 point precision, 12 is plenty to display + return BigDecimal.valueOf(value).round(displayRounding).stripTrailingZeros().toPlainString(); + } + catch (NumberFormatException exc) + { + throw new InternalExpressionException("Incorrect number format for " + value); + } + } + + @Override + public String getPrettyString() + { + return longValue != null || getDouble() == getLong() + ? Long.toString(getLong()) + : String.format(Locale.ROOT, "%.1f..", getDouble()); + } + + @Override + public boolean getBoolean() + { + return abs(value) > epsilon; + } + + public double getDouble() + { + return value; + } + + public float getFloat() + { + return (float) value; + } + + private static long floor(double v) + { + long invValue = (long) v; + return v < invValue ? invValue - 1 : invValue; + } + + public long getLong() + { + return longValue != null ? longValue : Long.valueOf(floor((value + epsilon))); + } + + @Override + public Value add(Value v) + { // TODO test if definintn add(NumericVlaue) woud solve the casting + if (v instanceof NumericValue nv) + { + return longValue != null && nv.longValue != null ? new NumericValue(longValue + nv.longValue) : new NumericValue(value + nv.value); + } + return super.add(v); + } + + @Override + public Value subtract(Value v) + { // TODO test if definintn add(NumericVlaue) woud solve the casting + if (v instanceof NumericValue nv) + { + return longValue != null && nv.longValue != null ? new NumericValue(longValue - nv.longValue) : new NumericValue(value - nv.value); + } + return super.subtract(v); + } + + @Override + public Value multiply(Value v) + { + if (v instanceof NumericValue nv) + { + return longValue != null && nv.longValue != null ? new NumericValue(longValue * nv.longValue) : new NumericValue(value * nv.value); + } + return v instanceof ListValue ? v.multiply(this) : new StringValue(StringUtils.repeat(v.getString(), (int) getLong())); + } + + @Override + public Value divide(Value v) + { + return v instanceof NumericValue nv ? new NumericValue(getDouble() / nv.getDouble()) : super.divide(v); + } + + @Override + public Value clone() + { + return new NumericValue(value, longValue); + } + + @Override + public int compareTo(Value o) + { + if (o.isNull()) + { + return -o.compareTo(this); + } + if (o instanceof NumericValue no) + { + return longValue != null && no.longValue != null ? longValue.compareTo(no.longValue) : Double.compare(value, no.value); + } + return getString().compareTo(o.getString()); + } + + @Override + public boolean equals(Object o) + { + if (o instanceof Value otherValue) + { + if (otherValue.isNull()) + { + return o.equals(this); + } + if (o instanceof NumericValue no) + { + if (longValue != null && no.longValue != null) + { + return longValue.equals(no.longValue); + } + if (Double.isNaN(this.value) || Double.isNaN(no.value)) + { + return false; + } + return !this.subtract(no).getBoolean(); + } + return super.equals(o); + } + return false; + } + + public NumericValue(double value) + { + this.value = value; + } + + private NumericValue(double value, Long longValue) + { + this.value = value; + this.longValue = longValue; + } + + public NumericValue(String value) + { + BigDecimal decimal = new BigDecimal(value); + if (decimal.stripTrailingZeros().scale() <= 0) + { + try + { + longValue = decimal.longValueExact(); + } + catch (ArithmeticException ignored) + { + } + } + this.value = decimal.doubleValue(); + } + + public NumericValue(long value) + { + this.longValue = value; + this.value = (double) value; + } + + @Override + public int length() + { + return Long.toString(getLong()).length(); + } + + @Override + public double readDoubleNumber() + { + return value; + } + + @Override + public long readInteger() + { + return getLong(); + } + + @Override + public String getTypeString() + { + return "number"; + } + + @Override + public int hashCode() + { + // is sufficiently close to the integer value + return longValue != null || Math.abs(Math.floor(value + 0.5D) - value) < epsilon ? Long.hashCode(getLong()) : Double.hashCode(value); + } + + + public int getInt() + { + return (int) getLong(); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (longValue != null) + { + if (abs(longValue) < Integer.MAX_VALUE - 2) + { + return IntTag.valueOf((int) (long) longValue); + } + return LongTag.valueOf(longValue); + } + long lv = getLong(); + if (value == (double) lv) + { + if (abs(value) < Integer.MAX_VALUE - 2) + { + return IntTag.valueOf((int) lv); + } + return LongTag.valueOf(getLong()); + } + else + { + return DoubleTag.valueOf(value); + } + } + + @Override + public JsonElement toJson() + { + if (longValue != null) + { + return new JsonPrimitive(longValue); + } + return isInteger() ? new JsonPrimitive(getLong()) : new JsonPrimitive(getDouble()); + } + + public NumericValue opposite() + { + return longValue != null ? new NumericValue(-longValue) : new NumericValue(-value); + } + + public boolean isInteger() + { + return longValue != null || getDouble() == getLong(); + } + + public Value mod(NumericValue n2) + { + if (this.longValue != null && n2.longValue != null) + { + return new NumericValue(Math.floorMod(longValue, n2.longValue)); + } + double x = value; + double y = n2.value; + if (y == 0) + { + throw new ArithmeticException("Division by zero"); + } + return new NumericValue(x - Math.floor(x / y) * y); + } +} diff --git a/src/main/java/carpet/script/value/ScreenValue.java b/src/main/java/carpet/script/value/ScreenValue.java new file mode 100644 index 0000000..a3c154f --- /dev/null +++ b/src/main/java/carpet/script/value/ScreenValue.java @@ -0,0 +1,560 @@ +package carpet.script.value; + +import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; +import carpet.script.Context; +import carpet.script.exception.IntegrityException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.InvalidCallbackException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.external.Vanilla; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.AbstractFurnaceMenu; +import net.minecraft.world.inventory.AnvilMenu; +import net.minecraft.world.inventory.BeaconMenu; +import net.minecraft.world.inventory.BlastFurnaceMenu; +import net.minecraft.world.inventory.BrewingStandMenu; +import net.minecraft.world.inventory.CartographyTableMenu; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.inventory.CraftingMenu; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.inventory.EnchantmentMenu; +import net.minecraft.world.inventory.FurnaceMenu; +import net.minecraft.world.inventory.GrindstoneMenu; +import net.minecraft.world.inventory.HopperMenu; +import net.minecraft.world.inventory.LecternMenu; +import net.minecraft.world.inventory.LoomMenu; +import net.minecraft.world.inventory.MerchantMenu; +import net.minecraft.world.inventory.ShulkerBoxMenu; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.SmithingMenu; +import net.minecraft.world.inventory.SmokerMenu; +import net.minecraft.world.inventory.StonecutterMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeHolder; + +import javax.annotation.Nullable; + +import static net.minecraft.world.inventory.MenuType.*; + +public class ScreenValue extends Value +{ + private AbstractContainerMenu screenHandler; + private ScreenHandlerInventory inventory; + + private final Component name; + private final String typestring; + private final FunctionValue callback; + private final String hostname; + private final ServerPlayer player; + private final CarpetScriptServer scriptServer; + + public static Map screenHandlerFactories; + + static + { + screenHandlerFactories = new HashMap<>(); + screenHandlerFactories.put("anvil", AnvilMenu::new); + screenHandlerFactories.put("beacon", BeaconMenu::new); + screenHandlerFactories.put("blast_furnace", BlastFurnaceMenu::new); + screenHandlerFactories.put("brewing_stand", (syncId, playerInventory) -> new BrewingStandMenu(syncId, playerInventory, new SimpleContainer(5), new SimpleContainerData(2))); + screenHandlerFactories.put("cartography_table", CartographyTableMenu::new); + screenHandlerFactories.put("crafting", CraftingMenu::new); + screenHandlerFactories.put("enchantment", EnchantmentMenu::new); + screenHandlerFactories.put("furnace", FurnaceMenu::new); + screenHandlerFactories.put("generic_3x3", ((syncId, playerInventory) -> new ChestMenu(GENERIC_3x3, syncId, playerInventory, new SimpleContainer(9), 1))); + screenHandlerFactories.put("generic_9x1", ((syncId, playerInventory) -> new ChestMenu(GENERIC_9x1, syncId, playerInventory, new SimpleContainer(9), 1))); + screenHandlerFactories.put("generic_9x2", ((syncId, playerInventory) -> new ChestMenu(GENERIC_9x2, syncId, playerInventory, new SimpleContainer(9 * 2), 2))); + screenHandlerFactories.put("generic_9x3", ((syncId, playerInventory) -> new ChestMenu(GENERIC_9x3, syncId, playerInventory, new SimpleContainer(9 * 3), 3))); + screenHandlerFactories.put("generic_9x4", ((syncId, playerInventory) -> new ChestMenu(GENERIC_9x4, syncId, playerInventory, new SimpleContainer(9 * 4), 4))); + screenHandlerFactories.put("generic_9x5", ((syncId, playerInventory) -> new ChestMenu(GENERIC_9x5, syncId, playerInventory, new SimpleContainer(9 * 5), 5))); + screenHandlerFactories.put("generic_9x6", ((syncId, playerInventory) -> new ChestMenu(GENERIC_9x6, syncId, playerInventory, new SimpleContainer(9 * 6), 6))); + screenHandlerFactories.put("grindstone", GrindstoneMenu::new); + screenHandlerFactories.put("hopper", HopperMenu::new); + screenHandlerFactories.put("lectern", (syncId, playerInventory) -> new LecternMenu(syncId, new SimpleContainer(1), new SimpleContainerData(1))); + screenHandlerFactories.put("loom", LoomMenu::new); + screenHandlerFactories.put("merchant", MerchantMenu::new); + screenHandlerFactories.put("shulker_box", (syncId, playerInventory) -> new ShulkerBoxMenu(syncId, playerInventory, new SimpleContainer(9 * 3))); + screenHandlerFactories.put("smithing", SmithingMenu::new); + screenHandlerFactories.put("smoker", SmokerMenu::new); + screenHandlerFactories.put("stonecutter", StonecutterMenu::new); + } + + + protected interface ScarpetScreenHandlerFactory + { + AbstractContainerMenu create(int syncId, Inventory playerInventory); + } + + public ScreenValue(ServerPlayer player, String type, Component name, @Nullable FunctionValue callback, Context c) + { + this.name = name; + this.typestring = type.toLowerCase(); + if (callback != null) + { + callback.checkArgs(4); + } + this.callback = callback; + this.hostname = c.host.getName(); + this.scriptServer = (CarpetScriptServer) c.host.scriptServer(); + this.player = player; + MenuProvider factory = this.createScreenHandlerFactory(); + if (factory == null) + { + throw new ThrowStatement(type, Throwables.UNKNOWN_SCREEN); + } + this.openScreen(factory); + this.inventory = new ScreenHandlerInventory(this.screenHandler); + } + + private MenuProvider createScreenHandlerFactory() + { + if (!screenHandlerFactories.containsKey(this.typestring)) + { + return null; + } + + return new SimpleMenuProvider((i, playerInventory, playerEntity) -> { + AbstractContainerMenu screen = screenHandlerFactories.get(ScreenValue.this.typestring).create(i, playerInventory); + ScreenValue.this.addListenerCallback(screen); + ScreenValue.this.screenHandler = screen; + return screen; + }, this.name); + } + + private void openScreen(MenuProvider factory) + { + if (this.player == null) + { + return; + } + OptionalInt optionalSyncId = this.player.openMenu(factory); + if (optionalSyncId.isPresent() && this.player.containerMenu.containerId == optionalSyncId.getAsInt()) + { + this.screenHandler = this.player.containerMenu; + } + } + + public void close() + { + if (this.player.containerMenu != this.player.inventoryMenu) + { + //prevent recursion when closing screen in closing screen callback by doing this before triggering event + this.inventory = null; + this.player.containerMenu = this.player.inventoryMenu; + this.player.closeContainer(); + this.screenHandler = null; + } + } + + public boolean isOpen() + { + if (this.screenHandler == null) + { + return false; + } + if (this.player.containerMenu.containerId == this.screenHandler.containerId) + { + return true; + } + this.screenHandler = null; + return false; + } + + + private boolean callListener(ServerPlayer player, String action, Map data) + { + Value playerValue = EntityValue.of(player); + Value actionValue = StringValue.of(action); + Value dataValue = MapValue.wrap(data); + List args = Arrays.asList(this, playerValue, actionValue, dataValue); + CarpetScriptHost appHost = scriptServer.getAppHostByName(this.hostname); + if (appHost == null) + { + this.close(); + this.screenHandler = null; + return false; + } + int runPermissionLevel = Vanilla.MinecraftServer_getRunPermissionLevel(player.server); + CommandSourceStack source = player.createCommandSourceStack().withPermission(runPermissionLevel); + CarpetScriptHost executingHost = appHost.retrieveForExecution(source, player); + try + { + Value cancelValue = executingHost.callUDF(source, callback, args); + return cancelValue.getString().equals("cancel"); + } + catch (NullPointerException | InvalidCallbackException | IntegrityException error) + { + CarpetScriptServer.LOG.error("Got exception when running screen event call ", error); + return false; + } + } + + private void addListenerCallback(AbstractContainerMenu screenHandler) + { + if (this.callback == null) + { + return; + } + + screenHandler.addSlotListener(new ScarpetScreenHandlerListener() + { + @Override + public boolean onSlotClick(ServerPlayer player, ClickType actionType, int slot, int button) + { + Map data = new HashMap<>(); + data.put(StringValue.of("slot"), slot == AbstractContainerMenu.SLOT_CLICKED_OUTSIDE ? Value.NULL : NumericValue.of(slot)); + if (actionType == ClickType.QUICK_CRAFT) + { + data.put(StringValue.of("quick_craft_stage"), NumericValue.of(AbstractContainerMenu.getQuickcraftHeader(button))); + button = AbstractContainerMenu.getQuickcraftType(button); + } + data.put(StringValue.of("button"), NumericValue.of(button)); + return ScreenValue.this.callListener(player, actionTypeToString(actionType), data); + } + + @Override + public boolean onButtonClick(ServerPlayer player, int button) + { + Map data = new HashMap<>(); + data.put(StringValue.of("button"), NumericValue.of(button)); + return ScreenValue.this.callListener(player, "button", data); + } + + @Override + public void onClose(ServerPlayer player) + { + Map data = new HashMap<>(); + ScreenValue.this.callListener(player, "close", data); + } + + @Override + public boolean onSelectRecipe(ServerPlayer player, RecipeHolder recipe, boolean craftAll) + { + Map data = new HashMap<>(); + data.put(StringValue.of("recipe"), ValueConversions.of(recipe.id())); + data.put(StringValue.of("craft_all"), BooleanValue.of(craftAll)); + return ScreenValue.this.callListener(player, "select_recipe", data); + } + + @Override + public void slotChanged(AbstractContainerMenu handler, int slotId, ItemStack stack) + { + Map data = new HashMap<>(); + data.put(StringValue.of("slot"), NumericValue.of(slotId)); + data.put(StringValue.of("stack"), ValueConversions.of(stack, player.level().registryAccess())); + ScreenValue.this.callListener(ScreenValue.this.player, "slot_update", data); + } + + @Override + public void dataChanged(AbstractContainerMenu handler, int property, int value) + { + } + }); + } + + private DataSlot getPropertyForType(Class screenHandlerClass, String requiredType, int propertyIndex, String propertyName) + { + if (screenHandlerClass.isInstance(this.screenHandler)) + { + return Vanilla.AbstractContainerMenu_getDataSlot(this.screenHandler, propertyIndex); + } + if (!this.isOpen()) + { + throw new InternalExpressionException("Screen property cannot be accessed, because the screen is already closed"); + } + throw new InternalExpressionException("Screen property " + propertyName + " expected a " + requiredType + " screen."); + } + + private DataSlot getProperty(String propertyName) + { + return switch (propertyName) + { + case "fuel_progress" -> getPropertyForType(AbstractFurnaceMenu.class, "furnace", 0, propertyName); + case "max_fuel_progress" -> getPropertyForType(AbstractFurnaceMenu.class, "furnace", 1, propertyName); + case "cook_progress" -> getPropertyForType(AbstractFurnaceMenu.class, "furnace", 2, propertyName); + case "max_cook_progress" -> getPropertyForType(AbstractFurnaceMenu.class, "furnace", 3, propertyName); + case "level_cost" -> getPropertyForType(AnvilMenu.class, "anvil", 0, propertyName); + case "page" -> getPropertyForType(LecternMenu.class, "lectern", 0, propertyName); + case "beacon_level" -> getPropertyForType(BeaconMenu.class, "beacon", 0, propertyName); + case "primary_effect" -> getPropertyForType(BeaconMenu.class, "beacon", 1, propertyName); + case "secondary_effect" -> getPropertyForType(BeaconMenu.class, "beacon", 2, propertyName); + case "brew_time" -> getPropertyForType(BrewingStandMenu.class, "brewing_stand", 0, propertyName); + case "brewing_fuel" -> getPropertyForType(BrewingStandMenu.class, "brewing_stand", 1, propertyName); + case "enchantment_power_1" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 0, propertyName); + case "enchantment_power_2" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 1, propertyName); + case "enchantment_power_3" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 2, propertyName); + case "enchantment_seed" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 3, propertyName); + case "enchantment_id_1" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 4, propertyName); + case "enchantment_id_2" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 5, propertyName); + case "enchantment_id_3" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 6, propertyName); + case "enchantment_level_1" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 7, propertyName); + case "enchantment_level_2" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 8, propertyName); + case "enchantment_level_3" -> getPropertyForType(EnchantmentMenu.class, "enchantment", 9, propertyName); + case "banner_pattern" -> getPropertyForType(LoomMenu.class, "loom", 0, propertyName); + case "stonecutter_recipe" -> getPropertyForType(StonecutterMenu.class, "stonecutter", 0, propertyName); + default -> throw new InternalExpressionException("Invalid screen property: " + propertyName); + }; + + } + + public Value queryProperty(String propertyName) + { + if (propertyName.equals("name")) + { + return FormattedTextValue.of(this.name); + } + if (propertyName.equals("open")) + { + return BooleanValue.of(this.isOpen()); + } + DataSlot property = getProperty(propertyName); + return NumericValue.of(property.get()); + } + + public Value modifyProperty(String propertyName, List lv) + { + DataSlot property = getProperty(propertyName); + int intValue = NumericValue.asNumber(lv.get(0)).getInt(); + property.set(intValue); + this.screenHandler.broadcastChanges(); + return Value.TRUE; + } + + public ServerPlayer getPlayer() + { + return this.player; + } + + public Container getInventory() + { + return this.inventory; + } + + @Override + public String getString() + { + return this.typestring + "_screen"; + } + + @Override + public boolean getBoolean() + { + return this.isOpen(); + } + + @Override + public String getTypeString() + { + return "screen"; + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (this.screenHandler == null) + { + return Value.NULL.toTag(true, regs); + } + + ListTag nbtList = new ListTag(); + for (int i = 0; i < this.screenHandler.slots.size(); i++) + { + ItemStack itemStack = this.screenHandler.getSlot(i).getItem(); + if (itemStack.isEmpty()) + { + nbtList.add(new CompoundTag()); + } + else + { + nbtList.add(itemStack.save(regs)); + } + } + return nbtList; + } + + + public interface ScarpetScreenHandlerListener extends ContainerListener + { + boolean onSlotClick(ServerPlayer player, ClickType actionType, int slot, int button); + + boolean onButtonClick(ServerPlayer player, int button); + + void onClose(ServerPlayer player); + + boolean onSelectRecipe(ServerPlayer player, RecipeHolder recipe, boolean craftAll); + } + + public static class ScreenHandlerInventory implements Container + { + protected AbstractContainerMenu screenHandler; + + public ScreenHandlerInventory(AbstractContainerMenu screenHandler) + { + this.screenHandler = screenHandler; + } + + @Override + public int getContainerSize() + { + return this.screenHandler.slots.size() + 1; + } + + @Override + public boolean isEmpty() + { + for (Slot slot : this.screenHandler.slots) + { + if (slot.hasItem() && !slot.getItem().isEmpty()) + { + return false; + } + } + return this.screenHandler.getCarried().isEmpty(); + } + + @Override + public ItemStack getItem(int slot) + { + if (slot == this.getContainerSize() - 1) + { + return this.screenHandler.getCarried(); + } + return slot >= -1 && slot < this.getContainerSize() ? this.screenHandler.slots.get(slot).getItem() : ItemStack.EMPTY; + } + + @Override + public ItemStack removeItem(int slot, int amount) + { + ItemStack itemStack; + if (slot == this.getContainerSize() - 1) + { + itemStack = this.screenHandler.getCarried().split(amount); + } + else + { + itemStack = ScreenHandlerInventory.splitStack(this.screenHandler.slots, slot, amount); + } + if (!itemStack.isEmpty()) + { + this.setChanged(); + } + return itemStack; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) + { + ItemStack itemStack; + if (slot == this.getContainerSize() - 1) + { + itemStack = this.screenHandler.getCarried(); + } + else + { + itemStack = this.screenHandler.slots.get(slot).getItem(); + } + if (itemStack.isEmpty()) + { + return ItemStack.EMPTY; + } + else + { + if (slot == this.getContainerSize() - 1) + { + this.screenHandler.setCarried(ItemStack.EMPTY); + } + else + { + this.screenHandler.slots.get(slot).set(ItemStack.EMPTY); + } + return itemStack; + } + } + + @Override + public void setItem(int slot, ItemStack stack) + { + if (slot == this.getContainerSize() - 1) + { + this.screenHandler.setCarried(stack); + } + else + { + this.screenHandler.slots.get(slot).set(stack); + } + if (!stack.isEmpty() && stack.getCount() > this.getMaxStackSize()) + { + stack.setCount(this.getMaxStackSize()); + } + + this.setChanged(); + } + + @Override + public void setChanged() + { + } + + @Override + public boolean stillValid(Player player) + { + return true; + } + + @Override + public void clearContent() + { + for (Slot slot : this.screenHandler.slots) + { + slot.set(ItemStack.EMPTY); + } + this.screenHandler.setCarried(ItemStack.EMPTY); + this.setChanged(); + } + + + public static ItemStack splitStack(List slots, int slot, int amount) + { + return slot >= 0 && slot < slots.size() && !slots.get(slot).getItem().isEmpty() && amount > 0 ? slots.get(slot).getItem().split(amount) : ItemStack.EMPTY; + } + } + + private static String actionTypeToString(ClickType actionType) + { + return switch (actionType) + { + case PICKUP -> "pickup"; + case QUICK_MOVE -> "quick_move"; + case SWAP -> "swap"; + case CLONE -> "clone"; + case THROW -> "throw"; + case QUICK_CRAFT -> "quick_craft"; + case PICKUP_ALL -> "pickup_all"; + }; + } +} diff --git a/src/main/java/carpet/script/value/StringValue.java b/src/main/java/carpet/script/value/StringValue.java new file mode 100644 index 0000000..df3df1d --- /dev/null +++ b/src/main/java/carpet/script/value/StringValue.java @@ -0,0 +1,54 @@ +package carpet.script.value; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; + +import javax.annotation.Nullable; + +public class StringValue extends Value +{ + public static Value EMPTY = StringValue.of(""); + + private final String str; + + @Override + public String getString() + { + return str; + } + + @Override + public boolean getBoolean() + { + return str != null && !str.isEmpty(); + } + + @Override + public Value clone() + { + return new StringValue(str); + } + + public StringValue(String str) + { + this.str = str; + } + + public static Value of(@Nullable String value) + { + return value == null ? Value.NULL : new StringValue(value); + } + + @Override + public String getTypeString() + { + return "string"; + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + return StringTag.valueOf(str); + } +} diff --git a/src/main/java/carpet/script/value/ThreadValue.java b/src/main/java/carpet/script/value/ThreadValue.java new file mode 100644 index 0000000..e566555 --- /dev/null +++ b/src/main/java/carpet/script/value/ThreadValue.java @@ -0,0 +1,285 @@ +package carpet.script.value; + +import carpet.script.Context; +import carpet.script.Expression; +import carpet.script.Tokenizer; +import carpet.script.exception.ExitStatement; +import carpet.script.exception.ExpressionException; +import carpet.script.exception.InternalExpressionException; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.Tag; + +public class ThreadValue extends LazyListValue +{ + private final CompletableFuture taskFuture; + private final long id; + private static long sequence = 0L; + private final Deque coState = new ArrayDeque<>(); + private final AtomicReference coLock = new AtomicReference<>(Value.EOL); + public final boolean isCoroutine; + + public ThreadValue(Value pool, FunctionValue function, Expression expr, Tokenizer.Token token, Context ctx, List args) + { + this.id = sequence++; + this.isCoroutine = ctx.host.canSynchronouslyExecute(); + this.taskFuture = getCompletableFutureFromFunction(pool, function, expr, token, ctx, args); + + Thread.yield(); + } + + public CompletableFuture getCompletableFutureFromFunction(Value pool, FunctionValue function, Expression expr, Tokenizer.Token token, Context ctx, List args) + { + ExecutorService executor = ctx.host.getExecutor(pool); + ThreadValue callingThread = isCoroutine ? this : null; + if (executor == null) + { + // app is shutting down - no more threads can be spawned. + return CompletableFuture.completedFuture(Value.NULL); + } + else + { + return CompletableFuture.supplyAsync(() -> { + try + { + return function.execute(ctx, Context.NONE, expr, token, args, callingThread).evalValue(ctx); + } + catch (ExitStatement exit) + { + // app stopped + return exit.retval; + } + catch (ExpressionException exc) + { + ctx.host.handleExpressionException("Thread failed\n", exc); + return Value.NULL; + } + }, ctx.host.getExecutor(pool)); + } + } + + @Override + public String getString() + { + return taskFuture.getNow(Value.NULL).getString(); + } + + public Value getValue() + { + return taskFuture.getNow(Value.NULL); + } + + @Override + public boolean getBoolean() + { + return taskFuture.getNow(Value.NULL).getBoolean(); + } + + public Value join() + { + try + { + return taskFuture.get(); + } + catch (ExitStatement exit) + { + taskFuture.complete(exit.retval); + return exit.retval; + } + catch (InterruptedException | ExecutionException e) + { + return Value.NULL; + } + } + + public boolean isFinished() + { + return taskFuture.isDone(); + } + + @Override + public boolean equals(Object o) + { + return o instanceof ThreadValue tv && tv.id == this.id; + } + + @Override + public int compareTo(Value o) + { + if (!(o instanceof ThreadValue tv)) + { + throw new InternalExpressionException("Cannot compare tasks to other types"); + } + return (int) (this.id - tv.id); + } + + @Override + public int hashCode() + { + return Long.hashCode(id); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + if (!force) + { + throw new NBTSerializableValue.IncompatibleTypeException(this); + } + return getValue().toTag(true, regs); + } + + @Override + public String getTypeString() + { + return "task"; + } + + @Override + public void fatality() + { + // we signal that won't be interested in the co-thread anymore + // but threads run client code, so we can't just kill them + } + + @Override + public void reset() + { + //throw new InternalExpressionException("Illegal operation on a task"); + } + + + @Override + public Iterator iterator() + { + if (!isCoroutine) + { + throw new InternalExpressionException("Cannot iterate over this task"); + } + return this; + } + + @Override + public boolean hasNext() + { + return !(coState.isEmpty() && taskFuture.isDone()); + } + + @Override + public Value next() + { + Value popped = null; + synchronized (coState) + { + while (true) + { + if (!coState.isEmpty()) + { + popped = coState.pop(); + } + else if (taskFuture.isDone()) + { + popped = Value.EOL; + } + if (popped != null) + { + break; + } + try + { + coState.wait(1); + } + catch (InterruptedException ignored) + { + } + } + coState.notifyAll(); + } + return popped; + } + + public void send(Value value) + { + synchronized (coLock) + { + coLock.set(value); + coLock.notifyAll(); + } + } + + public Value ping(Value value, boolean lock) + { + synchronized (coState) + { + try + { + if (!lock) + { + coState.add(value); + return Value.NULL; + } + while (true) + { + if (coState.isEmpty()) + { + coState.add(value); + break; + } + try + { + coState.wait(1); + } + + catch (InterruptedException ignored) + { + } + } + } + finally + { + coState.notifyAll(); + } + } + + // locked mode + + synchronized (coLock) + { + Value ret; + try + { + while (true) + { + Value current = coLock.get(); + if (current != Value.EOL) + { + ret = current; + coLock.set(Value.EOL); + break; + } + try + { + coLock.wait(1); + } + catch (InterruptedException ignored) + { + } + } + } + finally + { + coLock.notifyAll(); + } + return ret; + } + } +} diff --git a/src/main/java/carpet/script/value/UndefValue.java b/src/main/java/carpet/script/value/UndefValue.java new file mode 100644 index 0000000..6583d7d --- /dev/null +++ b/src/main/java/carpet/script/value/UndefValue.java @@ -0,0 +1,150 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import com.google.gson.JsonElement; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.Tag; + +public class UndefValue extends NullValue +{ + public static final UndefValue UNDEF = new UndefValue(); + public static final UndefValue EOL = new UndefValue(); + + private RuntimeException getError() + { + return new InternalExpressionException("variable " + boundVariable + " was used before initialization under 'strict' app config"); + } + + @Override + public String getString() + { + throw getError(); + } + + @Override + public String getPrettyString() + { + return "undefined"; + } + + @Override + public boolean getBoolean() + { + throw getError(); + } + + @Override + public Value clone() + { + return new UndefValue(); + } + + @Override + public boolean equals(Object o) + { + throw getError(); + } + + @Override + public Value slice(long fromDesc, Long toDesc) + { + throw getError(); + } + + @Override + public NumericValue opposite() + { + throw getError(); + } + + @Override + public int length() + { + throw getError(); + } + + @Override + public int compareTo(Value o) + { + throw getError(); + } + + @Override + public Value in(Value value) + { + throw getError(); + } + + @Override + public String getTypeString() + { + return "undef"; + } + + @Override + public int hashCode() + { + throw getError(); + } + + @Override + public Tag toTag(boolean force, RegistryAccess regs) + { + throw getError(); + } + + @Override + public Value split(Value delimiter) + { + throw getError(); + } + + @Override + public JsonElement toJson() + { + throw getError(); + } + + @Override + public boolean isNull() + { + throw getError(); + } + + @Override + public Value add(Value v) + { + throw getError(); + } + + @Override + public Value subtract(Value v) + { + throw getError(); + } + + @Override + public Value multiply(Value v) + { + throw getError(); + } + + @Override + public Value divide(Value v) + { + throw getError(); + } + + @Override + public double readDoubleNumber() + { + throw getError(); + } + + @Override + public long readInteger() + { + throw getError(); + } + +} diff --git a/src/main/java/carpet/script/value/Value.java b/src/main/java/carpet/script/value/Value.java new file mode 100644 index 0000000..3276b7f --- /dev/null +++ b/src/main/java/carpet/script/value/Value.java @@ -0,0 +1,279 @@ +package carpet.script.value; + +import carpet.script.CarpetScriptServer; +import carpet.script.exception.InternalExpressionException; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.Tag; + +import javax.annotation.Nullable; + +public abstract class Value implements Comparable, Cloneable +{ + public static final NumericValue FALSE = BooleanValue.FALSE; + public static final NumericValue TRUE = BooleanValue.TRUE; + public static final NumericValue ZERO = new NumericValue(0); + public static final NumericValue ONE = new NumericValue(1); + + public static final NullValue NULL = NullValue.NULL; + public static final UndefValue UNDEF = UndefValue.UNDEF; + public static final UndefValue EOL = UndefValue.EOL; + + public String boundVariable; + + public boolean isBound() + { + return boundVariable != null; + } + + public String getVariable() + { + return boundVariable; + } + + public Value reboundedTo(String value) + { + Value copy; + try + { + copy = (Value) clone(); + } + catch (CloneNotSupportedException e) + { + // should not happen + CarpetScriptServer.LOG.error("Failed to clone variable", e); + throw new InternalExpressionException("Variable of type " + getTypeString() + " is not cloneable. Tell gnembon about it, this shoudn't happen"); + } + copy.boundVariable = value; + return copy; + } + + public Value bindTo(String value) + { + this.boundVariable = value; + return this; + } + + public abstract String getString(); + + public String getPrettyString() + { + return getString(); + } + + + public abstract boolean getBoolean(); + + public Value add(Value o) + { + if (o instanceof FormattedTextValue) + { + return FormattedTextValue.combine(this, o); + } + String leftStr = this.getString(); + String rightStr = o.getString(); + return new StringValue(leftStr + rightStr); + } + + public Value subtract(Value v) + { + return new StringValue(this.getString().replace(v.getString(), "")); + } + + public Value multiply(Value v) + { + return v instanceof NumericValue || v instanceof ListValue + ? v.multiply(this) + : new StringValue(this.getString() + "." + v.getString()); + } + + public Value divide(Value v) + { + if (v instanceof NumericValue number) + { + String lstr = getString(); + return new StringValue(lstr.substring(0, (int) (lstr.length() / number.getDouble()))); + } + return new StringValue(getString() + "/" + v.getString()); + } + + public Value() + { + this.boundVariable = null; + } + + @Override + public int compareTo(Value o) + { + return o instanceof NumericValue || o instanceof ListValue || o instanceof ThreadValue + ? -o.compareTo(this) + : getString().compareTo(o.getString()); + } + + @Override // for hashmap key access, and == operator + public boolean equals(Object o) + { + if (o instanceof Value v) + { + return this.compareTo(v) == 0; + } + return false; + } + + public void assertAssignable() + { + if (boundVariable == null)// || boundVariable.startsWith("_")) + { + /*if (boundVariable != null) + { + throw new InternalExpressionException(boundVariable+ " cannot be assigned a new value"); + }*/ + throw new InternalExpressionException(getString() + " is not a variable"); + } + } + + public Value in(Value value1) + { + Pattern p; + try + { + p = Pattern.compile(value1.getString()); + } + catch (PatternSyntaxException pse) + { + throw new InternalExpressionException("Incorrect matching pattern: " + pse.getMessage()); + } + Matcher m = p.matcher(this.getString()); + if (!m.find()) + { + return Value.NULL; + } + int gc = m.groupCount(); + if (gc == 0) + { + return new StringValue(m.group()); + } + if (gc == 1) + { + return StringValue.of(m.group(1)); + } + List groups = new ArrayList<>(gc); + for (int i = 1; i <= gc; i++) + { + groups.add(StringValue.of(m.group(i))); + } + return ListValue.wrap(groups); + } + + public int length() + { + return getString().length(); + } + + public Value slice(long fromDesc, @Nullable Long toDesc) + { + String value = this.getString(); + 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 + 1); + if (from > to) + { + return StringValue.EMPTY; + } + return new StringValue(value.substring(from, to)); + } + + public Value split(@Nullable Value delimiter) + { + if (delimiter == null) + { + delimiter = StringValue.EMPTY; + } + try + { + return ListValue.wrap(Arrays.stream(getString().split(delimiter.getString())).map(StringValue::new)); + } + catch (PatternSyntaxException pse) + { + throw new InternalExpressionException("Incorrect pattern for 'split': " + pse.getMessage()); + } + } + + public double readDoubleNumber() + { + String s = getString(); + try + { + return Double.parseDouble(s); + } + catch (NumberFormatException e) + { + return Double.NaN; + } + } + + public long readInteger() + { + return (long) readDoubleNumber(); + } + + public String getTypeString() + { + throw new InternalExpressionException("How did you get here? Cannot get type of an intenal type."); + } + + @Override + public int hashCode() + { + String stringVal = getString(); + return stringVal.isEmpty() ? 0 : ("s" + stringVal).hashCode(); + } + + public Value deepcopy() + { + try + { + return (Value) this.clone(); + } + catch (CloneNotSupportedException e) + { + // should never happen + throw new InternalExpressionException("Cannot make a copy of value: " + this); + } + } + + public abstract Tag toTag(boolean force, RegistryAccess regs); + + public JsonElement toJson() + { + return new JsonPrimitive(getString()); + } + + public boolean isNull() + { + return false; + } + + /** + * @return retrieves useful in-run value of an optimized code-base value. + * For immutable values (most of them) it can return itself, + * but for mutables, it needs to be its copy or deep copy. + */ + public Value fromConstant() + { + return this; + } +} diff --git a/src/main/java/carpet/script/value/ValueConversions.java b/src/main/java/carpet/script/value/ValueConversions.java new file mode 100644 index 0000000..8bf58f1 --- /dev/null +++ b/src/main/java/carpet/script/value/ValueConversions.java @@ -0,0 +1,551 @@ +package carpet.script.value; + +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import carpet.script.external.Vanilla; +import carpet.script.utils.Colors; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.Vec3i; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ColumnPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.TagKey; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.ai.behavior.PositionTracker; +import net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities; +import net.minecraft.world.entity.ai.memory.WalkTarget; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.material.MapColor; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.stream.StreamSupport; + +import javax.annotation.Nullable; + +public class ValueConversions +{ + public static Value of(BlockPos pos) + { + return ListValue.of(new NumericValue(pos.getX()), new NumericValue(pos.getY()), new NumericValue(pos.getZ())); + } + + public static Value of(Vec3 vec) + { + return ListValue.of(new NumericValue(vec.x), new NumericValue(vec.y), new NumericValue(vec.z)); + } + + public static Value of(ColumnPos cpos) + { + return ListValue.of(new NumericValue(cpos.x()), new NumericValue(cpos.z())); + } + + public static Value of(ServerLevel world) + { + return of(world.dimension().location()); + } + + public static Value of(MapColor color) + { + return ListValue.of(StringValue.of(Colors.mapColourName.get(color)), ofRGB(color.col)); + } + + public static Value of(MinMaxBounds range) + { + return ListValue.of( + range.min().map(NumericValue::of).orElse(Value.NULL), + range.max().map(NumericValue::of).orElse(Value.NULL) + ); + } + + public static Value of(ItemStack stack, RegistryAccess regs) + { + return stack == null || stack.isEmpty() ? Value.NULL : ListValue.of( + of(regs.registryOrThrow(Registries.ITEM).getKey(stack.getItem())), + new NumericValue(stack.getCount()), + NBTSerializableValue.fromStack(stack, regs) + ); + } + + public static Value of(Objective objective) + { + return ListValue.of( + StringValue.of(objective.getName()), + StringValue.of(objective.getCriteria().getName()) + ); + } + + + public static Value of(ObjectiveCriteria criteria) + { + return ListValue.of( + StringValue.of(criteria.getName()), + BooleanValue.of(criteria.isReadOnly()) + ); + } + + + public static Value of(ParticleOptions particle, RegistryAccess regs) + { + String repr = ParticleTypes.CODEC.encodeStart(regs.createSerializationContext(NbtOps.INSTANCE), particle).toString(); + return StringValue.of(repr.startsWith("minecraft:") ? repr.substring(10) : repr); + } + + public static Value ofRGB(int value) + { + return new NumericValue(value * 256 + 255); + } + + public static Level dimFromValue(Value dimensionValue, MinecraftServer server) + { + if (dimensionValue instanceof EntityValue) + { + return ((EntityValue) dimensionValue).getEntity().getCommandSenderWorld(); + } + else if (dimensionValue instanceof BlockValue bv) + { + if (bv.getWorld() != null) + { + return bv.getWorld(); + } + else + { + throw new InternalExpressionException("dimension argument accepts only world-localized block arguments"); + } + } + else + { + String dimString = dimensionValue.getString().toLowerCase(Locale.ROOT); + return switch (dimString) { + case "nether", "the_nether" -> server.getLevel(Level.NETHER); + case "end", "the_end" -> server.getLevel(Level.END); + case "overworld", "over_world" -> server.getLevel(Level.OVERWORLD); + default -> { + ResourceKey dim = null; + ResourceLocation id = ResourceLocation.parse(dimString); + // not using RegistryKey.of since that one creates on check + for (ResourceKey world : (server.levelKeys())) + { + if (id.equals(world.location())) + { + dim = world; + break; + } + } + if (dim == null) + { + throw new ThrowStatement(dimString, Throwables.UNKNOWN_DIMENSION); + } + yield server.getLevel(dim); + } + }; + } + } + + public static Value of(ResourceKey dim) + { + return of(dim.location()); + } + + public static Value of(TagKey tagKey) + { + return of(tagKey.location()); + } + + public static Value of(@Nullable ResourceLocation id) + { + if (id == null) // should be Value.NULL + { + return Value.NULL; + } + return new StringValue(simplify(id)); + } + + public static String simplify(ResourceLocation id) + { + if (id == null) // should be Value.NULL + { + return ""; + } + if (id.getNamespace().equals("minecraft")) + { + return id.getPath(); + } + return id.toString(); + } + + public static Value of(GlobalPos pos) + { + return ListValue.of( + ValueConversions.of(pos.dimension()), + ValueConversions.of(pos.pos()) + ); + } + + public static Value fromPath(ServerLevel world, Path path) + { + List nodes = new ArrayList<>(); + for (int i = 0, len = path.getNodeCount(); i < len; i++) + { + Node node = path.getNode(i); + nodes.add(ListValue.of( + new BlockValue(null, world, node.asBlockPos()), + new StringValue(node.type.name().toLowerCase(Locale.ROOT)), + new NumericValue(node.costMalus), + BooleanValue.of(node.closed) + )); + } + return ListValue.wrap(nodes); + } + + public static Value fromTimedMemory(Entity e, long expiry, Object v) + { + Value ret = fromEntityMemory(e, v); + return ret.isNull() || expiry == Long.MAX_VALUE ? ret : ListValue.of(ret, new NumericValue(expiry)); + } + + private static Value fromEntityMemory(Entity e, Object v) + { + if (v instanceof GlobalPos pos) + { + return of(pos); + } + if (v instanceof final Entity entity) + { + return new EntityValue(entity); + } + if (v instanceof final BlockPos pos) + { + return new BlockValue(null, (ServerLevel) e.getCommandSenderWorld(), pos); + } + if (v instanceof final Number number) + { + return new NumericValue(number.doubleValue()); + } + if (v instanceof final Boolean bool) + { + return BooleanValue.of(bool); + } + if (v instanceof final UUID uuid) + { + return ofUUID((ServerLevel) e.getCommandSenderWorld(), uuid); + } + if (v instanceof final DamageSource source) + { + return ListValue.of( + new StringValue(source.getMsgId()), + source.getEntity() == null ? Value.NULL : new EntityValue(source.getEntity()) + ); + } + if (v instanceof final Path path) + { + return fromPath((ServerLevel) e.getCommandSenderWorld(), path); + } + if (v instanceof final PositionTracker tracker) + { + return new BlockValue(null, (ServerLevel) e.getCommandSenderWorld(), tracker.currentBlockPosition()); + } + if (v instanceof final WalkTarget target) + { + return ListValue.of( + new BlockValue(null, (ServerLevel) e.getCommandSenderWorld(), target.getTarget().currentBlockPosition()), + new NumericValue(target.getSpeedModifier()), + new NumericValue(target.getCloseEnoughDist()) + ); + } + if (v instanceof final NearestVisibleLivingEntities nvle) + { + v = StreamSupport.stream(nvle.findAll(entity -> true).spliterator(), false).toList(); + } + if (v instanceof final Set set) + { + v = new ArrayList<>(set); + } + if (v instanceof final List l) + { + if (l.isEmpty()) + { + return ListValue.of(); + } + Object el = l.get(0); + if (el instanceof final Entity entity) + { + return ListValue.wrap(l.stream().map(o -> new EntityValue(entity))); + } + if (el instanceof final GlobalPos pos) + { + return ListValue.wrap(l.stream().map(o -> of(pos))); + } + } + return Value.NULL; + } + + private static Value ofUUID(ServerLevel entityWorld, UUID uuid) + { + Entity current = entityWorld.getEntity(uuid); + return ListValue.of( + current == null ? Value.NULL : new EntityValue(current), + new StringValue(uuid.toString()) + ); + } + + public static Value of(AABB box) + { + return ListValue.of( + ListValue.fromTriple(box.minX, box.minY, box.minZ), + ListValue.fromTriple(box.maxX, box.maxY, box.maxZ) + ); + } + + public static Value of(BoundingBox box) + { + return ListValue.of( + ListValue.fromTriple(box.minX(), box.minY(), box.minZ()), + ListValue.fromTriple(box.maxX(), box.maxY(), box.maxZ()) + ); + } + + public static Value of(StructureStart structure, RegistryAccess regs) + { + if (structure == null || structure == StructureStart.INVALID_START) + { + return Value.NULL; + } + BoundingBox boundingBox = structure.getBoundingBox(); + if (boundingBox.maxX() < boundingBox.minX() || boundingBox.maxY() < boundingBox.minY() || boundingBox.maxZ() < boundingBox.minZ()) + { + return Value.NULL; + } + Map ret = new HashMap<>(); + ret.put(new StringValue("box"), of(boundingBox)); + List pieces = new ArrayList<>(); + for (StructurePiece piece : structure.getPieces()) + { + BoundingBox box = piece.getBoundingBox(); + if (box.maxX() >= box.minX() && box.maxY() >= box.minY() && box.maxZ() >= box.minZ()) + { + pieces.add(ListValue.of( + NBTSerializableValue.nameFromRegistryId(regs.registryOrThrow(Registries.STRUCTURE_PIECE).getKey(piece.getType())), + (piece.getOrientation() == null) ? Value.NULL : new StringValue(piece.getOrientation().getName()), + ListValue.fromTriple(box.minX(), box.minY(), box.minZ()), + ListValue.fromTriple(box.maxX(), box.maxY(), box.maxZ()) + )); + } + } + ret.put(new StringValue("pieces"), ListValue.wrap(pieces)); + return MapValue.wrap(ret); + } + + public static Value of(final ScoreHolder scoreHolder) + { + return FormattedTextValue.of(scoreHolder.getFeedbackDisplayName()); + } + + public static Value fromProperty(BlockState state, Property p) + { + Comparable object = state.getValue(p); + if (object instanceof Boolean || object instanceof Number) + { + return StringValue.of(object.toString()); + } + if (object instanceof final StringRepresentable stringRepresentable) + { + return StringValue.of(stringRepresentable.getSerializedName()); + } + throw new InternalExpressionException("Unknown property type: " + p.getName()); + } + + record SlotParam(/* Nullable */ String type, int id) + { + public ListValue build() + { + return ListValue.of(StringValue.of(type), new NumericValue(id)); + } + } + + private static final Int2ObjectMap slotIdsToSlotParams = new Int2ObjectOpenHashMap<>() + {{ + int n; + //covers blocks, player hotbar and inventory, and all default inventories + for (n = 0; n < 54; ++n) + { + put(n, new SlotParam(null, n)); + } + for (n = 0; n < 27; ++n) + { + put(200 + n, new SlotParam("enderchest", n)); + } + + // villager + for (n = 0; n < 8; ++n) + { + put(300 + n, new SlotParam(null, n)); + } + + // horse, llamas, donkeys, etc. + // two first slots are for saddle and armour + for (n = 0; n < 15; ++n) + { + put(500 + n, new SlotParam(null, n + 2)); + } + // weapon main hand + put(98, new SlotParam("equipment", 0)); + // offhand + put(99, new SlotParam("equipment", 5)); + // feet, legs, chest, head + for (n = 0; n < 4; ++n) + { + put(100 + n, new SlotParam("equipment", n + 1)); + } + //horse defaults saddle + put(400, new SlotParam(null, 0)); + // armor + put(401, new SlotParam(null, 1)); + // chest itself on the donkey is wierd - use NBT to alter that. + //hashMap.put("horse.chest", 499); + }}; + + public static Value ofVanillaSlotResult(int itemSlot) + { + SlotParam ret = slotIdsToSlotParams.get(itemSlot); + return ret == null ? ListValue.of(Value.NULL, new NumericValue(itemSlot)) : ret.build(); + } + + public static Value ofBlockPredicate(RegistryAccess registryAccess, Predicate blockPredicate) + { + Vanilla.BlockPredicatePayload payload = Vanilla.BlockPredicatePayload.of(blockPredicate); + Registry blocks = registryAccess.registryOrThrow(Registries.BLOCK); + return ListValue.of( + payload.state() == null ? Value.NULL : of(blocks.getKey(payload.state().getBlock())), + payload.tagKey() == null ? Value.NULL : of(blocks.getTag(payload.tagKey()).get().key()), + MapValue.wrap(payload.properties()), + payload.tag() == null ? Value.NULL : new NBTSerializableValue(payload.tag()) + ); + } + + public static ItemStack getItemStackFromValue(Value value, boolean withCount, RegistryAccess regs) + { + if (value.isNull()) + { + return ItemStack.EMPTY; + } + String name; + int count = 1; + CompoundTag nbtTag = null; + if (value instanceof ListValue list) + { + if (list.length() != 3) + { + throw new ThrowStatement("item definition from list of size " + list.length(), Throwables.UNKNOWN_ITEM); + } + List items = list.getItems(); + name = items.get(0).getString(); + if (withCount) + { + count = NumericValue.asNumber(items.get(1)).getInt(); + } + Value nbtValue = items.get(2); + if (!nbtValue.isNull()) + { + nbtTag = ((NBTSerializableValue) NBTSerializableValue.fromValue(nbtValue)).getCompoundTag(); + } + } + else + { + name = value.getString(); + } + ItemStack itemInput = NBTSerializableValue.parseItem(name, nbtTag, regs); + itemInput.setCount(count); + return itemInput; + } + + public static Value guess(ServerLevel serverWorld, Object o) + { + if (o == null) + { + return Value.NULL; + } + if (o instanceof final List list) + { + return ListValue.wrap(list.stream().map(oo -> guess(serverWorld, oo))); + } + if (o instanceof final BlockPos pos) + { + return new BlockValue(null, serverWorld, pos); + } + if (o instanceof final Entity e) + { + return EntityValue.of(e); + } + if (o instanceof final Vec3 vec3) + { + return of(vec3); + } + if (o instanceof final Vec3i vec3i) + { + return of(new BlockPos(vec3i)); + } + if (o instanceof final AABB aabb) + { + return of(aabb); + } + if (o instanceof final BoundingBox bb) + { + return of(bb); + } + if (o instanceof final ItemStack itemStack) + { + return of(itemStack, serverWorld.registryAccess()); + } + if (o instanceof final Boolean bool) + { + return BooleanValue.of(bool); + } + if (o instanceof final Number number) + { + return NumericValue.of(number); + } + if (o instanceof final ResourceLocation resourceLocation) + { + return of(resourceLocation); + } + return StringValue.of(o.toString()); + } +} diff --git a/src/main/java/carpet/script/value/package-info.java b/src/main/java/carpet/script/value/package-info.java new file mode 100644 index 0000000..8d3b588 --- /dev/null +++ b/src/main/java/carpet/script/value/package-info.java @@ -0,0 +1,8 @@ +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +package carpet.script.value; + +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/java/carpet/settings/Condition.java b/src/main/java/carpet/settings/Condition.java new file mode 100644 index 0000000..8f480b8 --- /dev/null +++ b/src/main/java/carpet/settings/Condition.java @@ -0,0 +1,20 @@ +package carpet.settings; + +import carpet.CarpetSettings; + +/** + * @deprecated Use {@link carpet.api.settings.Rule.Condition} instead + * + */ +@Deprecated(forRemoval = true) +public interface Condition extends carpet.api.settings.Rule.Condition { + boolean isTrue(); + + @Override + default boolean shouldRegister() { + CarpetSettings.LOG.warn(""" + Extension is referencing outdated Condition class! Class that caused this was: %s + This won't be supported in later Carpet versions and will crash the game!""".formatted(getClass().getName())); + return isTrue(); + } +} diff --git a/src/main/java/carpet/settings/ParsedRule.java b/src/main/java/carpet/settings/ParsedRule.java new file mode 100644 index 0000000..b02bfec --- /dev/null +++ b/src/main/java/carpet/settings/ParsedRule.java @@ -0,0 +1,511 @@ +package carpet.settings; + +import carpet.CarpetSettings; +import carpet.api.settings.CarpetRule; +import carpet.api.settings.InvalidRuleValueException; +import carpet.api.settings.RuleHelper; +import carpet.api.settings.SettingsManager; +import carpet.api.settings.Validators; +import carpet.utils.Messenger; +import carpet.utils.TranslationKeys; +import carpet.utils.Translations; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.ClassUtils; + +/** + * A Carpet rule parsed from a field, with its name, value, and other useful stuff. + * + * It is used for the fields with the {@link Rule} annotation + * when being parsed by {@link SettingsManager#parseSettingsClass(Class)}. + * + * @param The field's (and rule's) type + * @deprecated Use the type {@link CarpetRule} instead + */ +@Deprecated(forRemoval = true) // to move to api.settings package and visibility to package private +@SuppressWarnings("removal") // Gradle needs the explicit suppression +public final class ParsedRule implements CarpetRule, Comparable> { + private static final Map, FromStringConverter> CONVERTER_MAP = Map.ofEntries( + Map.entry(String.class, str -> str), + Map.entry(Boolean.class, str -> { + return switch (str) { + case "true" -> true; + case "false" -> false; + default -> throw new InvalidRuleValueException("Invalid boolean value"); + }; + }), + numericalConverter(Integer.class, Integer::parseInt), + numericalConverter(Double.class, Double::parseDouble), + numericalConverter(Long.class, Long::parseLong), + numericalConverter(Float.class, Float::parseFloat) + ); + /** + * @deprecated No replacement for this, since a {@link CarpetRule} may not always use a {@link Field}. + * Use {@link #value()} to access the rule's value + */ + @Deprecated(forRemoval = true) // to private + public final Field field; + /** + * @deprecated Use {@link CarpetRule#name()} instead + */ + @Deprecated(forRemoval = true) // to private + public final String name; + /** + * @deprecated Use {@link RuleHelper#translatedDescription(CarpetRule)}, or get it from the translation system + */ + @Deprecated(forRemoval = true) // to remove + public final String description; + /** + * @deprecated Use {@link CarpetRule#extraInfo()} instead + */ + @Deprecated(forRemoval = true) // to remove + public final List extraInfo; + /** + * @deprecated Use {@link CarpetRule#categories()} instead + */ + @Deprecated(forRemoval = true) // to private + public final List categories; + /** + * @deprecated Use {@link CarpetRule#suggestions()} instead + */ + @Deprecated(forRemoval = true) // to private (and rename?) + public final List options; + /** + * @deprecated Use {@link CarpetRule#strict()} instead + */ + @Deprecated(forRemoval = true) // to remove or fix + public boolean isStrict; + /** + * @deprecated Use {@link CarpetRule#canBeToggledClientSide()} instead + */ + @Deprecated(forRemoval = true) // to private (and maybe rename?) + public boolean isClient; + /** + * @deprecated Use {@link CarpetRule#type()} instead + */ + @Deprecated(forRemoval = true) // to private (or remove and delegate to typedfield?) + public final Class type; + /** + * @deprecated Use {@link CarpetRule#defaultValue()} instead + */ + @Deprecated(forRemoval = true) // to private + public final T defaultValue; + /** + * @deprecated Use {@link CarpetRule#settingsManager()} instead. + * This field may be {@code null} if the settings manager isn't an instance of the old type + */ + @Deprecated(forRemoval = true) // to remove in favour of realSettingsManager + public final carpet.settings.SettingsManager settingsManager; + /** + * @deprecated No replacement for this. A Carpet rule may not use {@link Validator} + */ + @Deprecated(forRemoval = true) // to remove (in favour of realValidators) + public final List> validators; + /** + * @deprecated Use {@link CarpetRule#defaultValue()} and pass it to {@link RuleHelper#toRuleString(Object)} instead + */ + @Deprecated(forRemoval = true) // to remove + public final String defaultAsString; + /** + * @deprecated No replacement for this, Scarpet Rules should be managed by the rule implementation + */ + @Deprecated(forRemoval = true) // to private/subclass + public final String scarpetApp; + private final FromStringConverter converter; + private final SettingsManager realSettingsManager; // to rename to settingsManager + /** + * If you reference this field I'll steal your kneecaps + */ + @Deprecated(forRemoval = true) + public final List> realValidators; // to rename to validators and to package private for printRulesToLog + private final boolean isLegacy; // to remove, only used for fallbacks + + @FunctionalInterface + interface FromStringConverter { + T convert(String value) throws InvalidRuleValueException; + } + + record RuleAnnotation(boolean isLegacy, String name, String desc, String[] extra, String[] category, String[] options, boolean strict, String appSource, Class[] validators) { + } + + /** + * If you call this method I'll steal your kneecaps + */ + @Deprecated(forRemoval = true) + public static ParsedRule of(Field field, SettingsManager settingsManager) { + RuleAnnotation rule; + if (field.isAnnotationPresent(carpet.api.settings.Rule.class)) { + carpet.api.settings.Rule a = field.getAnnotation(carpet.api.settings.Rule.class); + rule = new RuleAnnotation(false, null, null, null, a.categories(), a.options(), a.strict(), a.appSource(), a.validators()); + } else if (settingsManager instanceof carpet.settings.SettingsManager && field.isAnnotationPresent(Rule.class)) { // Legacy path + Rule a = field.getAnnotation(Rule.class); + rule = new RuleAnnotation(true, a.name(), a.desc(), a.extra(), a.category(), a.options(), a.strict(), a.appSource(), a.validate()); + } else { + // Don't allow to use old rule types in custom AND migrated settings manager + throw new IllegalArgumentException("Old rule annotation is only supported in legacy SettngsManager!"); + } + return new ParsedRule<>(field, rule, settingsManager); + } + + private ParsedRule(Field field, RuleAnnotation rule, SettingsManager settingsManager) + { + this.isLegacy = rule.isLegacy(); + this.name = !isLegacy || rule.name().isEmpty() ? field.getName() : rule.name(); + this.field = field; + @SuppressWarnings("unchecked") // We are "defining" T here + Class type = (Class)ClassUtils.primitiveToWrapper(field.getType()); + this.type = type; + this.isStrict = rule.strict(); + this.categories = List.of(rule.category()); + this.scarpetApp = rule.appSource(); + this.realSettingsManager = settingsManager; + if (!(settingsManager instanceof carpet.settings.SettingsManager)) { + // this is awkward... but people using a custom, new (extends only new api) manager should not be using this anyway but the interface method + this.settingsManager = null; + } else { + this.settingsManager = (carpet.settings.SettingsManager) settingsManager; + } + this.realValidators = Stream.of(rule.validators()).map(this::instantiateValidator).collect(Collectors.toList()); + this.defaultValue = value(); + FromStringConverter converter0 = null; + + if (categories.contains(RuleCategory.COMMAND)) + { + this.realValidators.add(new Validator._COMMAND()); + if (this.type == String.class) + { + this.realValidators.add(instantiateValidator(Validators.CommandLevel.class)); + } + } + + this.isClient = categories.contains(RuleCategory.CLIENT); + if (this.isClient) + { + this.realValidators.add(new Validator._CLIENT<>()); + } + + if (!scarpetApp.isEmpty()) + { + this.realValidators.add(new Validator.ScarpetValidator<>()); + } + + if (rule.options().length > 0) + { + this.options = List.of(rule.options()); + } + else if (this.type == Boolean.class) { + this.options = List.of("true", "false"); + } + else if (this.type == String.class && categories.contains(RuleCategory.COMMAND)) + { + this.options = Validators.CommandLevel.OPTIONS; + } + else if (this.type.isEnum()) + { + this.options = Arrays.stream(this.type.getEnumConstants()).map(e -> ((Enum) e).name().toLowerCase(Locale.ROOT)).toList(); + converter0 = str -> { + try { + @SuppressWarnings({"unchecked", "rawtypes"}) // Raw necessary because of signature. Unchecked because compiler doesn't know T extends Enum + T ret = (T)Enum.valueOf((Class) type, str.toUpperCase(Locale.ROOT)); + return ret; + } catch (IllegalArgumentException e) { + throw new InvalidRuleValueException("Valid values for this rule are: " + this.options); + } + }; + } + else + { + this.options = List.of(); + } + if (isStrict && !this.options.isEmpty()) + { + this.realValidators.add(0, new Validator.StrictValidator<>()); // at 0 prevents validators with side effects from running when invalid + } + if (converter0 == null) { + @SuppressWarnings("unchecked") + FromStringConverter converterFromMap = (FromStringConverter)CONVERTER_MAP.get(type); + if (converterFromMap == null) throw new UnsupportedOperationException("Unsupported type for ParsedRule" + type); + converter0 = converterFromMap; + } + this.converter = converter0; + + // Language "constants" + String nameKey = TranslationKeys.RULE_NAME_PATTERN.formatted(settingsManager().identifier(), name()); + String descKey = TranslationKeys.RULE_DESC_PATTERN.formatted(settingsManager().identifier(), name()); + String extraPrefix = TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name()); + + // to remove + this.description = isLegacy ? rule.desc() : Objects.requireNonNull(Translations.trOrNull(descKey), "No language key provided for " + descKey); + this.extraInfo = isLegacy ? List.of(rule.extra()) : getTranslationArray(extraPrefix); + this.defaultAsString = RuleHelper.toRuleString(this.defaultValue); + this.validators = realValidators.stream().filter(Validator.class::isInstance).map(v -> (Validator) v).toList(); + if (!isLegacy && !validators.isEmpty()) throw new IllegalArgumentException("Can't use legacy validators with new rules!"); + + // Language fallbacks - Also asserts the strings will be available in non-english languages, given current system has no fallback + if (isLegacy && !rule.name().isEmpty()) Translations.registerFallbackTranslation(nameKey, name); + Translations.registerFallbackTranslation(descKey, description); + Iterator infoIterator = extraInfo.iterator(); + for (int i = 0; infoIterator.hasNext(); i++) { + Translations.registerFallbackTranslation(extraPrefix + i, infoIterator.next()); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) // Needed because of the annotation + private carpet.api.settings.Validator instantiateValidator(Class cls) + { + try + { + Constructor constr = cls.getDeclaredConstructor(); + constr.setAccessible(true); + return constr.newInstance(); + } + catch (ReflectiveOperationException e) + { + throw new IllegalArgumentException(e); + } + } + + @Override + public void set(CommandSourceStack source, String value) throws InvalidRuleValueException + { + set(source, converter.convert(value), value); + } + + private void set(CommandSourceStack source, T value, String userInput) throws InvalidRuleValueException + { + for (carpet.api.settings.Validator validator : this.realValidators) + { + value = validator.validate(source, this, value, userInput); // should this recalculate the string? Another validator may have changed value + if (value == null) { + if (source != null) validator.notifyFailure(source, this, userInput); + throw new InvalidRuleValueException(); + } + } + if (!value.equals(value()) || source == null) + { + try { + this.field.set(null, value); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Couldn't access field for rule: " + name, e); + } + if (source != null) settingsManager().notifyRuleChanged(source, this, userInput); + } + } + + @Override + public boolean equals(Object obj) + { + return obj instanceof ParsedRule && ((ParsedRule) obj).name.equals(this.name); + } + + @Override + public int hashCode() + { + return this.name.hashCode(); + } + + @Override + public String toString() + { + return this.name + ": " + RuleHelper.toRuleString(value()); + } + + @Override + public String name() { + return name; + } + + @Override + public List extraInfo() { + return getTranslationArray(TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name())) + .stream() + .map(str -> Messenger.c("g " + str)) + .toList(); + } + + private List getTranslationArray(String prefix) { + List ret = new ArrayList<>(); + for (int i = 0; Translations.hasTranslation(prefix + i); i++) { + ret.add(Translations.tr(prefix + i)); + } + return ret; + } + + @Override + public Collection categories() { + return categories; + } + + @Override + public Collection suggestions() { + return options; + } + + @Override + public SettingsManager settingsManager() { + return realSettingsManager; + } + + @Override + @SuppressWarnings("unchecked") // T comes from the field + public T value() { + try { + return (T) field.get(null); + } catch (IllegalAccessException e) { + // Can't happen at regular runtime because we'd have thrown it on construction + throw new IllegalArgumentException("Couldn't access field for rule: " + name, e); + } + } + + @Override + public boolean canBeToggledClientSide() { + return isClient; + } + + @Override + public Class type() { + return type; + } + + @Override + public T defaultValue() { + return defaultValue; + } + + @Override + public void set(CommandSourceStack source, T value) throws InvalidRuleValueException { + set(source, value, RuleHelper.toRuleString(value)); + } + + @Override + public boolean strict() { + return !realValidators.isEmpty() && realValidators.get(0) instanceof Validator.StrictValidator; + } + + private static Map.Entry, FromStringConverter> numericalConverter(Class outputClass, Function converter) { + return Map.entry(outputClass, str -> { + try { + return converter.apply(str); + } catch (NumberFormatException e) { + throw new InvalidRuleValueException("Invalid number for rule"); + } + }); + } + + //TO REMOVE + + /** + * @deprecated Use {@link CarpetRule#value()} instead + */ + @Deprecated(forRemoval = true) + public T get() + { + return value(); + } + + /** + * @deprecated Use {@link RuleHelper#toRuleString(Object) RuleHelper.convertToRuleString(rule.value())} + */ + @Deprecated(forRemoval = true) + public String getAsString() + { + return RuleHelper.toRuleString(value()); + } + + /** + * @return The value of this {@link ParsedRule}, converted to a {@link boolean}. + * It will only return {@link true} if it's a true {@link boolean} or + * a number greater than zero. + * @deprecated Use {@link RuleHelper#getBooleanValue(CarpetRule)} + */ + @Deprecated(forRemoval = true) + public boolean getBoolValue() + { + return RuleHelper.getBooleanValue(this); + } + + /** + * @deprecated Use {@link RuleHelper#isInDefaultValue(CarpetRule)} + */ + @Deprecated(forRemoval = true) + public boolean isDefault() + { + return RuleHelper.isInDefaultValue(this); + } + + /** + * @deprecated Use {@link RuleHelper#resetToDefault(CarpetRule, CommandSourceStack)} + */ + @Deprecated(forRemoval = true) + public void resetToDefault(CommandSourceStack source) + { + RuleHelper.resetToDefault(this, source); + } + + /** + * @deprecated Forcing {@link Comparable} isn't a thing on {@link CarpetRule}s. Instead, pass a comparator by name to your + * sort methods, you can get one by calling {@code Comparator.comparing(CarpetRule::name)} + */ + @Override + @Deprecated(forRemoval = true) + public int compareTo(ParsedRule o) + { + if (!warnedComparable) { + warnedComparable = true; + CarpetSettings.LOG.warn(""" + Extension is relying on carpet rules to be comparable! This is not true for all carpet rules anymore, \ + and will crash the game in future versions or if an extension adds non-comparable rules! + Fixing it is as simple as passing Comparator.comparing(CarpetRule::name) to the sorting method!""", + new Throwable("Location:").fillInStackTrace()); + } + return this.name.compareTo(o.name); + } + private static boolean warnedComparable = false; + + /** + * @return A {@link String} being the translated {@link ParsedRule#name} of this rule, + * in Carpet's configured language. + * @deprecated Use {@link RuleHelper#translatedName(CarpetRule)} instead + */ + @Deprecated(forRemoval = true) + public String translatedName() { + return RuleHelper.translatedName(this); + } + + /** + * @return A {@link String} being the translated {@link ParsedRule#description description} of this rule, + * in Carpet's configured language. + * @deprecated Use {@link RuleHelper#translatedDescription(CarpetRule)} instead + */ + @Deprecated(forRemoval = true) + public String translatedDescription() + { + return RuleHelper.translatedDescription(this); + } + + /** + * @return A {@link String} being the translated {@link ParsedRule#extraInfo extraInfo} of this + * {@link ParsedRule}, in Carpet's configured language. + * @deprecated Use {@link CarpetRule#extraInfo()} instead + */ + @Deprecated(forRemoval = true) + public List translatedExtras() + { + return extraInfo().stream().map(Component::getString).toList(); + } +} diff --git a/src/main/java/carpet/settings/Rule.java b/src/main/java/carpet/settings/Rule.java new file mode 100644 index 0000000..40c9887 --- /dev/null +++ b/src/main/java/carpet/settings/Rule.java @@ -0,0 +1,75 @@ +package carpet.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Any field in this class annotated with this class is interpreted as a carpet rule. + * The field must be static and have a type of one of: + * - boolean + * - int + * - double + * - String + * - a subclass of Enum + * The default value of the rule will be the initial value of the field. + * + * @deprecated Use {@link carpet.api.settings.Rule} instead + */ +@Deprecated(forRemoval = true) +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Rule +{ + /** + * The rule name, by default the same as the field name + */ + String name() default ""; // default same as field name + + /** + * A description of the rule + */ + String desc(); + + /** + * Extra information about the rule + */ + String[] extra() default {}; + + /** + * A list of categories the rule is in + */ + String[] category(); + + /** + * Options to select in menu. + * Inferred for booleans and enums. Otherwise, must be present. + */ + String[] options() default {}; + + /** + * if a rule is not strict - can take any value, otherwise it needs to match + * any of the options + * For enums, its always strict, same for booleans - no need to set that for them. + */ + boolean strict() default true; + + /** + * If specified, the rule will automatically enable or disable + * a builtin Scarpet Rule App with this name. + */ + String appSource() default ""; + + /** + * The class of the validator checked when the rule is changed. + */ + @SuppressWarnings("rawtypes") + Class[] validate() default {}; + + /** + * The class of the condition checked when the rule is parsed, before being added + * to the Settings Manager. + */ + Class[] condition() default {}; +} diff --git a/src/main/java/carpet/settings/RuleCategory.java b/src/main/java/carpet/settings/RuleCategory.java new file mode 100644 index 0000000..8c5612e --- /dev/null +++ b/src/main/java/carpet/settings/RuleCategory.java @@ -0,0 +1,59 @@ +package carpet.settings; + +/** + * @deprecated Use {@link carpet.api.settings.RuleCategory} instead. Should be as simple as changing to that in the imports + * + */ +@Deprecated(forRemoval = true) +public class RuleCategory +{ + /** + * Use {@link carpet.api.settings.RuleCategory#BUGFIX} instead + */ + public static final String BUGFIX = carpet.api.settings.RuleCategory.BUGFIX; + /** + * Use {@link carpet.api.settings.RuleCategory#SURVIVAL} instead + */ + public static final String SURVIVAL = carpet.api.settings.RuleCategory.SURVIVAL; + /** + * Use {@link carpet.api.settings.RuleCategory#CREATIVE} instead + */ + public static final String CREATIVE = carpet.api.settings.RuleCategory.CREATIVE; + /** + * Use {@link carpet.api.settings.RuleCategory#EXPERIMENTAL} instead + */ + public static final String EXPERIMENTAL = carpet.api.settings.RuleCategory.EXPERIMENTAL; + /** + * Use {@link carpet.api.settings.RuleCategory#OPTIMIZATION} instead + */ + public static final String OPTIMIZATION = carpet.api.settings.RuleCategory.OPTIMIZATION; + /** + * Use {@link carpet.api.settings.RuleCategory#FEATURE} instead + */ + public static final String FEATURE = carpet.api.settings.RuleCategory.FEATURE; + /** + * Use {@link carpet.api.settings.RuleCategory#COMMAND} instead + */ + public static final String COMMAND = carpet.api.settings.RuleCategory.COMMAND; + /** + * Use {@link carpet.api.settings.RuleCategory#TNT} instead + */ + public static final String TNT = carpet.api.settings.RuleCategory.TNT; + /** + * Use {@link carpet.api.settings.RuleCategory#DISPENSER} instead + */ + public static final String DISPENSER = carpet.api.settings.RuleCategory.DISPENSER; + /** + * Use {@link carpet.api.settings.RuleCategory#SCARPET} instead + */ + public static final String SCARPET = carpet.api.settings.RuleCategory.SCARPET; + /** + * Rules with this {@link RuleCategory} will have a client-side + * counterpart, so they can be set independently without the server + * being Carpet's + * @deprecated Use {@link carpet.api.settings.RuleCategory#CLIENT} instead + */ + @Deprecated(forRemoval = true) + public static final String CLIENT = carpet.api.settings.RuleCategory.CLIENT; + +} diff --git a/src/main/java/carpet/settings/SettingsManager.java b/src/main/java/carpet/settings/SettingsManager.java new file mode 100644 index 0000000..ed2dbd9 --- /dev/null +++ b/src/main/java/carpet/settings/SettingsManager.java @@ -0,0 +1,123 @@ +package carpet.settings; + +import carpet.CarpetServer; +import carpet.utils.CommandHelper; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.flag.FeatureFlags; +import java.util.Collection; +import java.util.List; + +/** + * Manages and parses Carpet rules with their own command. + * @deprecated Use {@link carpet.api.settings.SettingsManager} instead + */ +@Deprecated(forRemoval = true) +@SuppressWarnings("removal") // Gradle needs the explicit suppression +public class SettingsManager extends carpet.api.settings.SettingsManager +{ + /** + * Creates a new {@link SettingsManager} without a fancy name. + * @see #SettingsManager(String, String, String) + * + * @param version A {@link String} with the mod's version + * @param identifier A {@link String} with the mod's id, will be the command name + * @deprecated This type is deprecated, use {@link carpet.api.settings.SettingsManager#SettingsManager(String, String, String)} instead + */ + @Deprecated(forRemoval = true) + public SettingsManager(String version, String identifier) + { + this(version, identifier, identifier); + } + + /** + * Creates a new {@link SettingsManager} with a fancy name + * + * @param version A {@link String} with the mod's version + * @param identifier A {@link String} with the mod's id, will be the command name + * @param fancyName A {@link String} being the mod's fancy name. + * + * @deprecated This type is deprecated, use {@link carpet.api.settings.SettingsManager#SettingsManager(String, String, String)} instead + */ + @Deprecated(forRemoval = true) + public SettingsManager(String version, String identifier, String fancyName) + { + super(version, identifier, fancyName); + } + + /** + * @deprecated Use {@link #identifier()} instead + */ + @Deprecated(forRemoval = true) + public String getIdentifier() { + return identifier(); + } + + /** + *

Gets a registered {@link ParsedRule} in this {@link SettingsManager} by its name.

+ * @param name The rule name + * @return The {@link ParsedRule} with that name + * @deprecated Use {@link #getCarpetRule(String)} instead. This method is not able to return rules not implemented by {@link ParsedRule} + */ + @Deprecated(forRemoval = true) + public ParsedRule getRule(String name) + { + return getCarpetRule(name) instanceof ParsedRule pr ? pr : null; + } + + /** + * @return A {@link Collection} of the registered {@link ParsedRule}s in this {@link SettingsManager}. + * @deprecated Use {@link #getCarpetRules()} instead. This method won't be able to return rules not implemented by {@link ParsedRule} + */ + @Deprecated(forRemoval = true) + public Collection> getRules() + { + return List.of(getCarpetRules().stream().filter(ParsedRule.class::isInstance).map(ParsedRule.class::cast).toArray(ParsedRule[]::new)); + } + + /** + * @deprecated Use {@link #dumpAllRulesToStream(java.io.PrintStream, String)} instead + */ + @Deprecated(forRemoval = true) + public int printAllRulesToLog(String category) { + return dumpAllRulesToStream(System.out, category); + } + + /** + * Notifies all players that the commands changed by resending the command tree. + * @deprecated While there's not an API replacement for this (at least yet), + * you can use {@link CommandHelper#notifyPlayersCommandsChanged(MinecraftServer)} instead + */ + @Deprecated(forRemoval = true) + public void notifyPlayersCommandsChanged() + { + CommandHelper.notifyPlayersCommandsChanged(CarpetServer.minecraft_server); + } + + /** + * Returns whether the {@link CommandSourceStack} can execute + * a command given the required permission level, according to + * Carpet's standard for permissions. + * @param source The origin {@link CommandSourceStack} + * @param commandLevel The permission level + * @return Whether or not the {@link CommandSourceStack} meets the required level + * + * @deprecated While there's not an API replacement for this (at least yet), + * you can use {@link CommandHelper#canUseCommand(CommandSourceStack, Object)} instead + */ + @Deprecated(forRemoval = true) + public static boolean canUseCommand(CommandSourceStack source, Object commandLevel) + { + return CommandHelper.canUseCommand(source, commandLevel); + } + + @Deprecated(forRemoval = true) + public void registerCommand(CommandDispatcher dispatcher) + { + final CommandBuildContext context = CommandBuildContext.simple(RegistryAccess.EMPTY, FeatureFlags.VANILLA_SET); + registerCommand(dispatcher, context); + } +} diff --git a/src/main/java/carpet/settings/Validator.java b/src/main/java/carpet/settings/Validator.java new file mode 100644 index 0000000..be4fc40 --- /dev/null +++ b/src/main/java/carpet/settings/Validator.java @@ -0,0 +1,114 @@ +package carpet.settings; + +import carpet.CarpetSettings; +import carpet.api.settings.CarpetRule; +import carpet.api.settings.Validators; +import carpet.utils.CommandHelper; +import carpet.utils.Messenger; +import net.minecraft.commands.CommandSourceStack; + +/** + * @deprecated Use {@link carpet.api.settings.Validator} instead + */ +@Deprecated(forRemoval = true) +@SuppressWarnings("removal") // Gradle needs the explicit suppression +public abstract class Validator extends carpet.api.settings.Validator +{ + { + // Print deprecation warning once while instantiating the class + CarpetSettings.LOG.warn(""" + Validator '%s' is implementing the old Validator class! This class is deprecated and will be removed \ + and crash in later Carpet versions!""".formatted(getClass().getName())); + } + /** + * Validate the new value of a rule + * @return a value if the given one was valid or could be cleanly adapted, null if new value is invalid. + */ + @Override + public final T validate(CommandSourceStack source, CarpetRule changingRule, T newValue, String stringInput) { + // Compatibility code + if (!(changingRule instanceof ParsedRule parsedRule)) + // Throwing here is not an issue because Carpet's current implementation only calls validators with ParsedRule. + // This would be thrown if a different implementation tries to use it, and then it's their issue in multiple ways + throw new IllegalArgumentException("Passed a non-ParsedRule to a validator using the outdated method!"); + return validate(source, parsedRule, newValue, stringInput); + } + /** + * @deprecated Implement {@link #validate(CommandSourceStack, CarpetRule, Object, String)} instead! It will get abstract soon! + */ + @Deprecated(forRemoval = true) + public abstract T validate(CommandSourceStack source, ParsedRule currentRule, T newValue, String string); + + /** + * @deprecated Use {@link Validators.CommandLevel} instead + */ + @Deprecated(forRemoval = true) // to remove + public static class _COMMAND_LEVEL_VALIDATOR extends Validators.CommandLevel {} + + /** + * @deprecated Use {@link Validators.NonNegativeNumber} instead + */ + @Deprecated(forRemoval = true) // to remove + public static class NONNEGATIVE_NUMBER extends Validators.NonNegativeNumber {} + + /** + * @deprecated Use {@link Validators.Probablity} instead + */ + @Deprecated(forRemoval = true) // to remove + public static class PROBABILITY extends Validators.Probablity {} + + // The ones below are part of the implementation of ParsedRule or printRulesToLog, so they need to be close to it to stay hidden + // They will need to be moved when moving ParsedRule + + static class _COMMAND extends carpet.api.settings.Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) + { + if (source != null) + CommandHelper.notifyPlayersCommandsChanged(source.getServer()); + return newValue; + } + @Override + public String description() { return "It has an accompanying command";} + } + + // maybe remove this one and make printRulesToLog check for canBeToggledClientSide instead + static class _CLIENT extends carpet.api.settings.Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) + { + return newValue; + } + @Override + public String description() { return "Its a client command so can be issued and potentially be effective when connecting to non-carpet/vanilla servers. " + + "In these situations (on vanilla servers) it will only affect the executing player, so each player needs to type it" + + " separately for the desired effect";} + } + + static class ScarpetValidator extends carpet.api.settings.Validator { //TODO remove? The additional info isn't that useful tbh + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) + { + return newValue; + } + @Override public String description() { + return "It controls an accompanying Scarpet App"; + } + } + + static class StrictValidator extends carpet.api.settings.Validator + { + @Override + public T validate(CommandSourceStack source, CarpetRule currentRule, T newValue, String string) + { + if (!currentRule.suggestions().contains(string)) + { + Messenger.m(source, "r Valid options: " + currentRule.suggestions().toString()); + return null; + } + return newValue; + } + } +} diff --git a/src/main/java/carpet/utils/BlockInfo.java b/src/main/java/carpet/utils/BlockInfo.java new file mode 100644 index 0000000..e1519e8 --- /dev/null +++ b/src/main/java/carpet/utils/BlockInfo.java @@ -0,0 +1,112 @@ +package carpet.utils; + +import java.util.ArrayList; +import java.util.List; + +import carpet.script.utils.Colors; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.goal.RandomStrollGoal; +import net.minecraft.world.entity.ai.util.DefaultRandomPos; +import net.minecraft.world.entity.monster.ZombifiedPiglin; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.PathComputationType; +import net.minecraft.world.phys.Vec3; + +public class BlockInfo +{ + public static List blockInfo(BlockPos pos, ServerLevel world) + { + BlockState state = world.getBlockState(pos); + Block block = state.getBlock(); + String metastring = ""; + final Registry blocks = world.registryAccess().registryOrThrow(Registries.BLOCK); + for (net.minecraft.world.level.block.state.properties.Property iproperty : state.getProperties()) + { + metastring += ", "+iproperty.getName() + '='+state.getValue(iproperty); + } + List lst = new ArrayList<>(); + lst.add(Messenger.s("")); + lst.add(Messenger.s("=====================================")); + lst.add(Messenger.s(String.format("Block info for %s%s (id %d%s):", blocks.getKey(block),metastring, blocks.getId(block), metastring ))); + lst.add(Messenger.s(String.format(" - Map colour: %s", Colors.mapColourName.get(state.getMapColor(world, pos))))); + lst.add(Messenger.s(String.format(" - Sound type: %s", Colors.soundName.get(state.getSoundType())))); + lst.add(Messenger.s("")); + lst.add(Messenger.s(String.format(" - Full block: %s", state.isCollisionShapeFullBlock(world, pos)))); // isFullCube() ))); + lst.add(Messenger.s(String.format(" - Normal cube: %s", state.isRedstoneConductor(world, pos)))); //isNormalCube()))); isSimpleFullBlock + lst.add(Messenger.s(String.format(" - Is liquid: %s", state.is(Blocks.WATER) || state.is(Blocks.LAVA)))); + lst.add(Messenger.s("")); + lst.add(Messenger.s(String.format(" - Light in: %d, above: %d", + Math.max(world.getBrightness(LightLayer.BLOCK, pos),world.getBrightness(LightLayer.SKY, pos)) , + Math.max(world.getBrightness(LightLayer.BLOCK, pos.above()),world.getBrightness(LightLayer.SKY, pos.above()))))); + lst.add(Messenger.s(String.format(" - Brightness in: %.2f, above: %.2f", world.getLightLevelDependentMagicValue(pos), world.getLightLevelDependentMagicValue(pos.above())))); + lst.add(Messenger.s(String.format(" - Is opaque: %s", state.isSolid() ))); + //lst.add(Messenger.s(String.format(" - Light opacity: %d", state.getOpacity(world,pos)))); + //lst.add(Messenger.s(String.format(" - Emitted light: %d", state.getLightValue()))); + //lst.add(Messenger.s(String.format(" - Picks neighbour light value: %s", state.useNeighborBrightness(world, pos)))); + lst.add(Messenger.s("")); + lst.add(Messenger.s(String.format(" - Causes suffocation: %s", state.isSuffocating(world, pos)))); //canSuffocate + lst.add(Messenger.s(String.format(" - Blocks movement on land: %s", !state.isPathfindable(PathComputationType.LAND)))); + lst.add(Messenger.s(String.format(" - Blocks movement in air: %s", !state.isPathfindable(PathComputationType.AIR)))); + lst.add(Messenger.s(String.format(" - Blocks movement in liquids: %s", !state.isPathfindable(PathComputationType.WATER)))); + lst.add(Messenger.s(String.format(" - Can burn: %s", state.ignitedByLava()))); + lst.add(Messenger.s(String.format(" - Hardness: %.2f", state.getDestroySpeed(world, pos)))); + lst.add(Messenger.s(String.format(" - Blast resistance: %.2f", block.getExplosionResistance()))); + lst.add(Messenger.s(String.format(" - Ticks randomly: %s", state.isRandomlyTicking()))); + lst.add(Messenger.s("")); + lst.add(Messenger.s(String.format(" - Can provide power: %s", state.isSignalSource()))); + lst.add(Messenger.s(String.format(" - Strong power level: %d", world.getDirectSignalTo(pos)))); + lst.add(Messenger.s(String.format(" - Redstone power level: %d", world.getBestNeighborSignal(pos)))); + lst.add(Messenger.s("")); + lst.add(wander_chances(pos.above(), world)); + + return lst; + } + + private static Component wander_chances(BlockPos pos, ServerLevel worldIn) + { + PathfinderMob creature = new ZombifiedPiglin(EntityType.ZOMBIFIED_PIGLIN, worldIn); + creature.finalizeSpawn(worldIn, worldIn.getCurrentDifficultyAt(pos), MobSpawnType.NATURAL, null); + creature.moveTo(pos, 0.0F, 0.0F); + RandomStrollGoal wander = new RandomStrollGoal(creature, 0.8D); + int success = 0; + for (int i=0; i<1000; i++) + { + + Vec3 vec = DefaultRandomPos.getPos(creature, 10, 7); // TargetFinder.findTarget(creature, 10, 7); + if (vec == null) + { + continue; + } + success++; + } + long total_ticks = 0; + for (int trie=0; trie<1000; trie++) + { + int i; + for (i=1;i<30*20*60; i++) //*60 used to be 5 hours, limited to 30 mins + { + if (wander.canUse()) + { + break; + } + } + total_ticks += 3*i; + } + creature.discard(); // discarded // remove(Entity.RemovalReason.field_26999); // 2nd option - DISCARDED + long total_time = (total_ticks)/1000/20; + return Messenger.s(String.format(" - Wander chance above: %.1f%%\n - Average standby above: %s", + (100.0F*success)/1000, + ((total_time>5000)?"INFINITY":(total_time +" s")) + )); + } +} diff --git a/src/main/java/carpet/utils/CarpetProfiler.java b/src/main/java/carpet/utils/CarpetProfiler.java new file mode 100644 index 0000000..a4a0206 --- /dev/null +++ b/src/main/java/carpet/utils/CarpetProfiler.java @@ -0,0 +1,345 @@ +package carpet.utils; + +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.Comparator; +import java.util.Map; + +import static java.util.Map.entry; + +public class CarpetProfiler +{ + private static final Object2LongOpenHashMap SECTION_STATS = new Object2LongOpenHashMap<>(); + private static final Object2LongOpenHashMap> ENTITY_TIMES = new Object2LongOpenHashMap<>(); + private static final Object2LongOpenHashMap> ENTITY_COUNT = new Object2LongOpenHashMap<>(); + + + private static CommandSourceStack currentRequester = null; + public static int tick_health_requested = 0; + private static int tick_health_elapsed = 0; + private static TYPE test_type = TYPE.NONE; //1 for ticks, 2 for entities + private static long current_tick_start = 0; + private static final Map GENERAL_SECTIONS = Map.of( + "Network", "Packet sending, player logins, disconnects, kicks, anti-cheat check for player movement, etc.", + "Autosave", "Autosave", + "Async Tasks", "Various asynchronous tasks on the server. Mainly chunk generation, chunk saving, etc.", + "Datapacks", "Datapack tick function execution. Load function execution if reload was performed.", + "Carpet", "Player hud, scripts, and extensions (If they choose to use carpet's onTick)." + ); + + private static final Map SCARPET_SECTIONS = Map.of( + "Scarpet run", "script run command execution", + "Scarpet events", "script events, custom or built-in", + "Scarpet schedule", "script scheduled calls/events", + "Scarpet command", "script custom commands. Calls, executions, suggestions, etc.", + "Scarpet load", "script and libraries (if required) loading", + "Scarpet app data", "script module data (if required) ticking and saving", + "Scarpet client", "script shape rendering. (Client side)" + ); + + private static final Map SECTIONS = Map.ofEntries( + entry("Spawning", "Spawning of various things. Natural mobs, cats, patrols, wandering traders, phantoms, skeleton horses, etc."), + entry("Random Ticks", "Random ticks. Both block random ticks and fluid random ticks."), + entry("Ticket Manager", "Chunk ticket manager. Assigning tickets, removing tickets, etc."), + entry("Unloading", "POI ticking and chunk unloading."), + entry("Schedule Ticks", "Scheduled ticks. Repeaters, observers, redstone torch, water, lava, etc."), + entry("Block Events", "Scheduled Block events. Pistons, comparators, noteblocks, block entity events (chests opening/closing), etc."), + entry("Entities", "All the entities in the server. Ticking, removing, despawning, dragon fight (if active), etc."), + entry("Block Entities", "All the block entities in the server. Removal, ticking, etc."), + entry("Entities (Client)", "Entity lag client side. Mostly rendering."), + entry("Block Entities (Client)", "Block entity lag client side. Mostly rendering."), + entry("Raid", "Raid ticking, stopping, etc."), + entry("Environment", "Weather, time, waking up players, water freezing, cauldron filling, snow layers, etc.") + ); + + public enum TYPE + { + NONE, + GENERAL, + ENTITY, + TILEENTITY + } + + public static record ProfilerToken(TYPE type, Object section, long start, Level world) + { + public ProfilerToken(TYPE type, Object section, Level world) + { + this(type, section, System.nanoTime(), world); + } + } + + public static void prepare_tick_report(CommandSourceStack source, int ticks) + { + //maybe add so it only spams the sending player, but honestly - all may want to see it + SECTION_STATS.clear(); // everything then defaults to 0 + ENTITY_COUNT.clear(); + ENTITY_TIMES.clear(); + test_type = TYPE.GENERAL; + + tick_health_elapsed = ticks; + tick_health_requested = ticks; + current_tick_start = 0L; + currentRequester = source; + } + + public static void prepare_entity_report(CommandSourceStack source, int ticks) + { + //maybe add so it only spams the sending player, but honestly - all may want to see it + SECTION_STATS.clear(); + ENTITY_COUNT.clear(); + ENTITY_TIMES.clear(); + test_type = TYPE.ENTITY; + tick_health_elapsed = ticks; + tick_health_requested = ticks; + current_tick_start = 0L; + currentRequester = source; + } + + public static ProfilerToken start_section(Level world, String name, TYPE type) + { + if (tick_health_requested == 0L || test_type != TYPE.GENERAL || current_tick_start == 0) + return null; + return new ProfilerToken(type, name, world); + } + + public static ProfilerToken start_entity_section(Level world, Entity e, TYPE type) + { + if (tick_health_requested == 0L || test_type != TYPE.ENTITY || current_tick_start == 0) + return null; + return new ProfilerToken(type, e.getType(), world); + } + + public static ProfilerToken start_block_entity_section(Level world, BlockEntity be, TYPE type) + { + if (tick_health_requested == 0L || test_type != TYPE.ENTITY || current_tick_start == 0) + return null; + return new ProfilerToken(type, be.getType(), world); + } + + public static void end_current_section(ProfilerToken tok) + { + if (tick_health_requested == 0L || test_type != TYPE.GENERAL || current_tick_start == 0 || tok == null) + return; + long end_time = System.nanoTime(); + if (tok.type == TYPE.GENERAL) + { + Level world = tok.world; + String current_section = (world == null) ? + (String) tok.section : + String.format("%s.%s%s", world.dimension().location(), tok.section, world.isClientSide ? " (Client)" : ""); + SECTION_STATS.addTo(current_section, end_time - tok.start); + } + } + + public static void end_current_entity_section(ProfilerToken tok) + { + if (tick_health_requested == 0L || test_type != TYPE.ENTITY || current_tick_start == 0 || tok == null) + return; + long end_time = System.nanoTime(); + Pair section = Pair.of(tok.world, tok.section); + ENTITY_TIMES.addTo(section, end_time - tok.start); + ENTITY_COUNT.addTo(section, 1); + } + + public static void start_tick_profiling() + { + current_tick_start = System.nanoTime(); + } + + public static void end_tick_profiling(MinecraftServer server) + { + if (current_tick_start == 0L) + return; + SECTION_STATS.addTo("tick", System.nanoTime() - current_tick_start); + tick_health_elapsed--; + if (tick_health_elapsed <= 0) + { + finalize_tick_report(server); + } + } + + public static void finalize_tick_report(MinecraftServer server) + { + if (test_type == TYPE.GENERAL) + finalize_tick_report_for_time(server); + if (test_type == TYPE.ENTITY) + finalize_tick_report_for_entities(server); + cleanup_tick_report(); + } + + public static void cleanup_tick_report() + { + SECTION_STATS.clear(); + ENTITY_TIMES.clear(); + ENTITY_COUNT.clear(); + test_type = TYPE.NONE; + tick_health_elapsed = 0; + tick_health_requested = 0; + current_tick_start = 0L; + currentRequester = null; + } + + public static void finalize_tick_report_for_time(MinecraftServer server) + { + //print stats + if (currentRequester == null) + return; + long total_tick_time = SECTION_STATS.getLong("tick"); + double divider = 1.0D / tick_health_requested / 1000000; + Messenger.m(currentRequester, "w "); + Messenger.m(currentRequester, "wb Average tick time: ", String.format("yb %.3fms", divider * total_tick_time)); + long accumulated = 0L; + + for (String section : GENERAL_SECTIONS.keySet()) + { + double amount = divider * SECTION_STATS.getLong(section); + if (amount > 0.01) + { + accumulated += SECTION_STATS.getLong(section); + Messenger.m( + currentRequester, + "w " + section + ": ", + "^ " + GENERAL_SECTIONS.get(section), + "y %.3fms".formatted(amount) + ); + } + } + for (String section : SCARPET_SECTIONS.keySet()) + { + double amount = divider * SECTION_STATS.getLong(section); + if (amount > 0.01) + { + Messenger.m( + currentRequester, + "gi "+section+": ", + "^ " + SCARPET_SECTIONS.get(section), + "di %.3fms".formatted(amount) + ); + } + } + + for (ResourceKey dim : server.levelKeys()) + { + ResourceLocation dimensionId = dim.location(); + boolean hasSomethin = false; + for (String section : SECTIONS.keySet()) + { + double amount = divider * SECTION_STATS.getLong(dimensionId + "." + section); + if (amount > 0.01) + { + hasSomethin = true; + break; + } + } + if (!(hasSomethin)) + { + continue; + } + Messenger.m(currentRequester, "wb "+(dimensionId.getNamespace().equals("minecraft")?dimensionId.getPath():dimensionId.toString()) + ":"); + for (String section : SECTIONS.keySet()) + { + double amount = divider * SECTION_STATS.getLong(dimensionId + "." + section); + if (amount > 0.01) + { + boolean cli = section.endsWith("(Client)"); + if (!cli) + accumulated += SECTION_STATS.getLong(dimensionId + "." + section); + Messenger.m( + currentRequester, + "%s - %s: ".formatted(cli ? "gi" : "w", section), + "^ " + SECTIONS.get(section), + "%s %.3fms".formatted(cli ? "di" : "y", amount) + ); + } + } + } + + long rest = total_tick_time - accumulated; + + Messenger.m(currentRequester, String.format("gi The Rest, whatever that might be: %.3fms", divider * rest)); + } + + private static String sectionName(Pair section) + { + ResourceLocation id; + final RegistryAccess regs = section.getKey().registryAccess(); + if (section.getValue() instanceof EntityType) + { + id = regs.registryOrThrow(Registries.ENTITY_TYPE).getKey((EntityType) section.getValue()); + } + else + { + id = regs.registryOrThrow(Registries.BLOCK_ENTITY_TYPE).getKey((BlockEntityType) section.getValue()); + } + String name = "minecraft".equals(id.getNamespace())?id.getPath():id.toString(); + if (section.getKey().isClientSide) + { + name += " (client)"; + } + ResourceLocation dimkey = section.getKey().dimension().location(); + String dim = "minecraft".equals(dimkey.getNamespace())?dimkey.getPath():dimkey.toString(); + return name+" in "+dim; + } + + public static void finalize_tick_report_for_entities(MinecraftServer server) + { + if (currentRequester == null) + return; + long total_tick_time = SECTION_STATS.getLong("tick"); + double divider = 1.0D / tick_health_requested / 1000000; + double divider_1 = 1.0D / (tick_health_requested - 1) / 1000000; + Messenger.m(currentRequester, "w "); + Messenger.m(currentRequester, "wb Average tick time: ", String.format("yb %.3fms", divider * total_tick_time)); + SECTION_STATS.removeLong("tick"); + Messenger.m(currentRequester, "wb Top 10 counts:"); + int total = 0; + for (Object2LongMap.Entry> sectionEntry : sortedByValue(ENTITY_COUNT)) + { + if (++total > 10) break; + Pair section = sectionEntry.getKey(); + boolean cli = section.getKey().isClientSide; + Messenger.m(currentRequester, String.format( + "%s - %s: ", cli?"gi":"w", + sectionName(section)), + String.format("%s %.1f", cli?"di":"y", + 1.0D * sectionEntry.getLongValue() / (tick_health_requested - (cli? 1 : 0)) + )); + } + Messenger.m(currentRequester, "wb Top 10 CPU hogs:"); + total = 0; + for (Object2LongMap.Entry> sectionEntry : sortedByValue(ENTITY_TIMES)) + { + if (++total > 10) break; + Pair section = sectionEntry.getKey(); + boolean cli = section.getKey().isClientSide; + Messenger.m(currentRequester, String.format( + "%s - %s: ", cli?"gi":"w", + sectionName(section)), + String.format("%s %.2fms", cli?"di":"y", + (cli ? divider : divider_1) * sectionEntry.getLongValue() + )); + } + } + + private static Iterable> sortedByValue(Object2LongMap mapToSort) { + return () -> mapToSort + .object2LongEntrySet() + .stream() + .sorted(Comparator.>comparingLong(Object2LongMap.Entry::getLongValue).reversed()) + .iterator(); + } +} diff --git a/src/main/java/carpet/utils/CarpetRulePrinter.java b/src/main/java/carpet/utils/CarpetRulePrinter.java new file mode 100644 index 0000000..bd36f11 --- /dev/null +++ b/src/main/java/carpet/utils/CarpetRulePrinter.java @@ -0,0 +1,68 @@ +package carpet.utils; + +import carpet.CarpetServer; +import joptsimple.OptionParser; +import joptsimple.OptionSet; +import joptsimple.OptionSpec; +import joptsimple.util.PathConverter; +import joptsimple.util.PathProperties; +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.loader.api.FabricLoader; + +import java.io.IOException; +import java.io.PrintStream; +import java.lang.System; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides a command line interface to generate a dump with all rules + * in a pretty markdown format to a specified file, with an optional + * category filter + * + */ +public class CarpetRulePrinter implements DedicatedServerModInitializer { + @Override + public void onInitializeServer() { + // When launching, we use the "--" separator to prevent the game rejecting to launch because of unknown options + // Clear it in case it's present given else our option parser would also ignore them! + String[] args = Arrays.stream(FabricLoader.getInstance().getLaunchArguments(true)).filter(opt -> !opt.equals("--")).toArray(String[]::new); + + // Prepare an OptionParser for our parameters + OptionParser parser = new OptionParser(); + OptionSpec shouldDump = parser.accepts("carpetDumpRules"); + OptionSpec pathSpec = parser.accepts("dumpPath").withRequiredArg().withValuesConvertedBy(new PathConverter()); + OptionSpec filterSpec = parser.accepts("dumpFilter").withRequiredArg(); + parser.allowsUnrecognizedOptions(); // minecraft may need more stuff later that we don't want to special-case + OptionSet options = parser.parse(args); + // If our flag isn't set, continue regular launch + if (!options.has(shouldDump)) return; + + + Logger logger = LoggerFactory.getLogger("Carpet Rule Printer"); + logger.info("Starting in rule dump mode..."); + // at this point, onGameStarted() already ran given it as an entrypoint runs before + PrintStream outputStream; + try { + Path path = options.valueOf(pathSpec).toAbsolutePath(); + logger.info("Printing rules to: " + path); + Files.createDirectories(path.getParent()); + outputStream = new PrintStream(Files.newOutputStream(path)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + // Ensure translations fallbacks have been generated given we run before the validator that ensures that has. + // Remove after removing old setting system, given there'll be no fallbacks + Translations.updateLanguage(); + String filter = options.valueOf(filterSpec); + if (filter != null) logger.info("Applying category filter: " + filter); + CarpetServer.settingsManager.dumpAllRulesToStream(outputStream, filter); + outputStream.close(); + logger.info("Rules have been printed"); + System.exit(0); + } +} diff --git a/src/main/java/carpet/utils/CommandHelper.java b/src/main/java/carpet/utils/CommandHelper.java new file mode 100644 index 0000000..d6825a9 --- /dev/null +++ b/src/main/java/carpet/utils/CommandHelper.java @@ -0,0 +1,56 @@ +package carpet.utils; + +import carpet.CarpetSettings; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.TickTask; +import net.minecraft.server.level.ServerPlayer; + +/** + * A few helpful methods to work with settings and commands. + * + * This is not any kind of API, but it's unlikely to change + * + */ +public final class CommandHelper { + private CommandHelper() {} + /** + * Notifies all players that the commands changed by resending the command tree. + */ + public static void notifyPlayersCommandsChanged(MinecraftServer server) + { + if (server == null || server.getPlayerList() == null) + { + return; + } + server.tell(new TickTask(server.getTickCount(), () -> + { + try { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + server.getCommands().sendCommands(player); + } + } + catch (NullPointerException e) + { + CarpetSettings.LOG.warn("Exception while refreshing commands, please report this to Carpet", e); + } + })); + } + + /** + * Whether the given source has enough permission level to run a command that requires the given commandLevel + */ + public static boolean canUseCommand(CommandSourceStack source, Object commandLevel) + { + if (commandLevel instanceof Boolean) return (Boolean) commandLevel; + String commandLevelString = commandLevel.toString(); + return switch (commandLevelString) + { + case "true" -> true; + case "false" -> false; + case "ops" -> source.hasPermission(2); // typical for other cheaty commands + case "0", "1", "2", "3", "4" -> source.hasPermission(Integer.parseInt(commandLevelString)); + default -> false; + }; + } +} diff --git a/src/main/java/carpet/utils/DistanceCalculator.java b/src/main/java/carpet/utils/DistanceCalculator.java new file mode 100644 index 0000000..5e665ac --- /dev/null +++ b/src/main/java/carpet/utils/DistanceCalculator.java @@ -0,0 +1,63 @@ +package carpet.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; + +public class DistanceCalculator +{ + public static final HashMap START_POINT_STORAGE = new HashMap<>(); + + public static boolean hasStartingPoint(CommandSourceStack source) + { + return START_POINT_STORAGE.containsKey(source.getTextName()); + } + + public static List findDistanceBetweenTwoPoints(Vec3 pos1, Vec3 pos2) + { + double dx = Mth.abs((float)pos1.x-(float)pos2.x); + double dy = Mth.abs((float)pos1.y-(float)pos2.y); + double dz = Mth.abs((float)pos1.z-(float)pos2.z); + double manhattan = dx+dy+dz; + double spherical = Math.sqrt(dx*dx + dy*dy + dz*dz); + double cylindrical = Math.sqrt(dx*dx + dz*dz); + List res = new ArrayList<>(); + res.add(Messenger.c("w Distance between ", + Messenger.tp("c",pos1),"w and ", + Messenger.tp("c",pos2),"w :")); + res.add(Messenger.c("w - Spherical: ", String.format("wb %.2f", spherical))); + res.add(Messenger.c("w - Cylindrical: ", String.format("wb %.2f", cylindrical))); + res.add(Messenger.c("w - Manhattan: ", String.format("wb %.1f", manhattan))); + return res; + } + + public static int distance(CommandSourceStack source, Vec3 pos1, Vec3 pos2) + { + Messenger.send(source, findDistanceBetweenTwoPoints(pos1, pos2)); + return 1; + } + + public static int setStart(CommandSourceStack source, Vec3 pos) + { + START_POINT_STORAGE.put(source.getTextName(), pos); + Messenger.m(source,"gi Initial point set to: ", Messenger.tp("g",pos)); + return 1; + } + + public static int setEnd(CommandSourceStack source, Vec3 pos) + { + if ( !hasStartingPoint(source) ) + { + START_POINT_STORAGE.put(source.getTextName(), pos); + Messenger.m(source,"gi There was no initial point for "+source.getTextName()); + Messenger.m(source,"gi Initial point set to: ", Messenger.tp("g",pos)); + return 0; + } + Messenger.send(source, findDistanceBetweenTwoPoints( START_POINT_STORAGE.get(source.getTextName()), pos)); + return 1; + } +} diff --git a/src/main/java/carpet/utils/EvictingQueue.java b/src/main/java/carpet/utils/EvictingQueue.java new file mode 100644 index 0000000..f313aa7 --- /dev/null +++ b/src/main/java/carpet/utils/EvictingQueue.java @@ -0,0 +1,20 @@ +package carpet.utils; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class EvictingQueue extends LinkedHashMap +{ + public void put(K key) + { + super.put(key, 1); + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) + { + return this.size() > 10; + } + + +} \ No newline at end of file diff --git a/src/main/java/carpet/utils/FabricAPIHooks.java b/src/main/java/carpet/utils/FabricAPIHooks.java new file mode 100644 index 0000000..8ece719 --- /dev/null +++ b/src/main/java/carpet/utils/FabricAPIHooks.java @@ -0,0 +1,44 @@ +package carpet.utils; +/* +import carpet.network.CarpetClient; +import com.mojang.blaze3d.systems.RenderSystem; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.SemanticVersion; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +*/ +public class FabricAPIHooks { +/* + public static final boolean WORLD_RENDER_EVENTS = hasMod("fabric-rendering-v1", "1.5.0"); + + private FabricAPIHooks() { + } + + public static void initialize() { + if (WORLD_RENDER_EVENTS) { + //WorldRenderEvents.BEFORE_DEBUG_RENDER.register(context -> { + if (false) {//(CarpetClient.shapes != null) { // likely won't need it. + CarpetClient.shapes.render(context.matrixStack(), context.camera(), context.tickDelta()); + } + }); + } + } + + private static boolean hasMod(String id, String minimumVersion) { + return FabricLoader.getInstance().getModContainer(id).map(m -> { + Version version = m.getMetadata().getVersion(); + + if (version instanceof SemanticVersion) { + try { + return ((SemanticVersion) version).compareTo(SemanticVersion.parse(minimumVersion)) >= 0; + } catch (VersionParsingException ignored) { + } + } + + return false; + }).orElse(false); + } + + */ +} diff --git a/src/main/java/carpet/utils/Messenger.java b/src/main/java/carpet/utils/Messenger.java new file mode 100644 index 0000000..36af4c1 --- /dev/null +++ b/src/main/java/carpet/utils/Messenger.java @@ -0,0 +1,320 @@ +package carpet.utils; + +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Messenger +{ + public static final Logger LOG = LoggerFactory.getLogger("Messaging System"); + + private static final Pattern colorExtract = Pattern.compile("#([0-9a-fA-F]{6})"); + public enum CarpetFormatting + { + ITALIC ('i', (s, f) -> s.withItalic(true)), + STRIKE ('s', (s, f) -> s.applyFormat(ChatFormatting.STRIKETHROUGH)), + UNDERLINE ('u', (s, f) -> s.applyFormat(ChatFormatting.UNDERLINE)), + BOLD ('b', (s, f) -> s.withBold(true)), + OBFUSCATE ('o', (s, f) -> s.applyFormat(ChatFormatting.OBFUSCATED)), + + WHITE ('w', (s, f) -> s.withColor(ChatFormatting.WHITE)), + YELLOW ('y', (s, f) -> s.withColor(ChatFormatting.YELLOW)), + LIGHT_PURPLE('m', (s, f) -> s.withColor(ChatFormatting.LIGHT_PURPLE)), // magenta + RED ('r', (s, f) -> s.withColor(ChatFormatting.RED)), + AQUA ('c', (s, f) -> s.withColor(ChatFormatting.AQUA)), // cyan + GREEN ('l', (s, f) -> s.withColor(ChatFormatting.GREEN)), // lime + BLUE ('t', (s, f) -> s.withColor(ChatFormatting.BLUE)), // light blue, teal + DARK_GRAY ('f', (s, f) -> s.withColor(ChatFormatting.DARK_GRAY)), + GRAY ('g', (s, f) -> s.withColor(ChatFormatting.GRAY)), + GOLD ('d', (s, f) -> s.withColor(ChatFormatting.GOLD)), + DARK_PURPLE ('p', (s, f) -> s.withColor(ChatFormatting.DARK_PURPLE)), // purple + DARK_RED ('n', (s, f) -> s.withColor(ChatFormatting.DARK_RED)), // brown + DARK_AQUA ('q', (s, f) -> s.withColor(ChatFormatting.DARK_AQUA)), + DARK_GREEN ('e', (s, f) -> s.withColor(ChatFormatting.DARK_GREEN)), + DARK_BLUE ('v', (s, f) -> s.withColor(ChatFormatting.DARK_BLUE)), // navy + BLACK ('k', (s, f) -> s.withColor(ChatFormatting.BLACK)), + + COLOR ('#', (s, f) -> { + TextColor color; + try + { + color = TextColor.parseColor("#" + f).getOrThrow(RuntimeException::new); + } + catch (RuntimeException e) + { + return s; + } + return color == null ? s : s.withColor(color); + }, s -> { + Matcher m = colorExtract.matcher(s); + return m.find() ? m.group(1) : null; + }), + ; + + public char code; + public BiFunction applier; + public Function container; + CarpetFormatting(char code, BiFunction applier) + { + this(code, applier, s -> s.indexOf(code)>=0?Character.toString(code):null); + } + CarpetFormatting(char code, BiFunction applier, Function container) + { + this.code = code; + this.applier = applier; + this.container = container; + } + public Style apply(String format, Style previous) + { + String fmt; + if ((fmt = container.apply(format))!= null) return applier.apply(previous, fmt); + return previous; + } + }; + + public static Style parseStyle(String style) + { + Style myStyle= Style.EMPTY.withColor(ChatFormatting.WHITE); + for (CarpetFormatting cf: CarpetFormatting.values()) myStyle = cf.apply(style, myStyle); + return myStyle; + } + public static String heatmap_color(double actual, double reference) + { + String color = "g"; + if (actual >= 0.0D) color = "e"; + if (actual > 0.5D*reference) color = "y"; + if (actual > 0.8D*reference) color = "r"; + if (actual > reference) color = "m"; + return color; + } + public static String creatureTypeColor(MobCategory type) + { + return switch (type) + { + case MONSTER -> "n"; + case CREATURE -> "e"; + case AMBIENT -> "f"; + case WATER_CREATURE -> "v"; + case WATER_AMBIENT -> "q"; + default -> "w"; // missing MISC and UNDERGROUND_WATER_CREATURE + }; + } + + private static MutableComponent getChatComponentFromDesc(String message, MutableComponent previousMessage) + { + if (message.equalsIgnoreCase("")) + { + return Component.literal(""); + } + if (Character.isWhitespace(message.charAt(0))) + { + message = "w" + message; + } + int limit = message.indexOf(' '); + String desc = message; + String str = ""; + if (limit >= 0) + { + desc = message.substring(0, limit); + str = message.substring(limit+1); + } + if (previousMessage == null) { + MutableComponent text = Component.literal(str); + text.setStyle(parseStyle(desc)); + return text; + } + Style previousStyle = previousMessage.getStyle(); + MutableComponent ret = previousMessage; + previousMessage.setStyle(switch (desc.charAt(0)) { + case '?' -> previousStyle.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, message.substring(1))); + case '!' -> previousStyle.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, message.substring(1))); + case '^' -> previousStyle.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, c(message.substring(1)))); + case '@' -> previousStyle.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, message.substring(1))); + case '&' -> previousStyle.withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, message.substring(1))); + default -> { // Create a new component + ret = Component.literal(str); + ret.setStyle(parseStyle(desc)); + yield previousStyle; // no op for the previous style + } + }); + return ret; + } + public static Component tp(String desc, Vec3 pos) { return tp(desc, pos.x, pos.y, pos.z); } + public static Component tp(String desc, BlockPos pos) { return tp(desc, pos.getX(), pos.getY(), pos.getZ()); } + public static Component tp(String desc, double x, double y, double z) { return tp(desc, (float)x, (float)y, (float)z);} + public static Component tp(String desc, float x, float y, float z) + { + return getCoordsTextComponent(desc, x, y, z, false); + } + public static Component tp(String desc, int x, int y, int z) + { + return getCoordsTextComponent(desc, x, y, z, true); + } + + /// to be continued + public static Component dbl(String style, double double_value) + { + return c(String.format("%s %.1f",style,double_value),String.format("^w %f",double_value)); + } + public static Component dbls(String style, double ... doubles) + { + StringBuilder str = new StringBuilder(style + " [ "); + String prefix = ""; + for (double dbl : doubles) + { + str.append(String.format("%s%.1f", prefix, dbl)); + prefix = ", "; + } + str.append(" ]"); + return c(str.toString()); + } + public static Component dblf(String style, double ... doubles) + { + StringBuilder str = new StringBuilder(style + " [ "); + String prefix = ""; + for (double dbl : doubles) + { + str.append(String.format("%s%f", prefix, dbl)); + prefix = ", "; + } + str.append(" ]"); + return c(str.toString()); + } + public static Component dblt(String style, double ... doubles) + { + List components = new ArrayList<>(); + components.add(style+" [ "); + String prefix = ""; + for (double dbl:doubles) + { + + components.add(String.format("%s %s%.1f",style, prefix, dbl)); + components.add("?"+dbl); + components.add("^w "+dbl); + prefix = ", "; + } + //components.remove(components.size()-1); + components.add(style+" ]"); + return c(components.toArray(new Object[0])); + } + + private static Component getCoordsTextComponent(String style, float x, float y, float z, boolean isInt) + { + String text; + String command; + if (isInt) + { + text = String.format("%s [ %d, %d, %d ]",style, (int)x,(int)y, (int)z ); + command = String.format("!/tp %d %d %d",(int)x,(int)y, (int)z); + } + else + { + text = String.format("%s [ %.1f, %.1f, %.1f]",style, x, y, z); + command = String.format("!/tp %.3f %.3f %.3f",x, y, z); + } + return c(text, command); + } + + //message source + public static void m(CommandSourceStack source, Object ... fields) + { + if (source != null) + source.sendSuccess(() -> Messenger.c(fields), source.getServer() != null && source.getServer().overworld() != null); + } + public static void m(Player player, Object ... fields) + { + player.sendSystemMessage(Messenger.c(fields)); + } + + /* + composes single line, multicomponent message, and returns as one chat messagge + */ + public static Component c(Object ... fields) + { + MutableComponent message = Component.literal(""); + MutableComponent previousComponent = null; + for (Object o: fields) + { + if (o instanceof MutableComponent) + { + message.append((MutableComponent)o); + previousComponent = (MutableComponent)o; + continue; + } + String txt = o.toString(); + MutableComponent comp = getChatComponentFromDesc(txt, previousComponent); + if (comp != previousComponent) message.append(comp); + previousComponent = comp; + } + return message; + } + + //simple text + + public static Component s(String text) + { + return s(text,""); + } + public static Component s(String text, String style) + { + MutableComponent message = Component.literal(text); + message.setStyle(parseStyle(style)); + return message; + } + + + + + public static void send(Player player, Collection lines) + { + lines.forEach(message -> player.sendSystemMessage(message)); + } + public static void send(CommandSourceStack source, Collection lines) + { + lines.stream().forEachOrdered((s) -> source.sendSuccess(() -> s, false)); + } + + + public static void print_server_message(MinecraftServer server, String message) + { + if (server == null) + LOG.error("Message not delivered: "+message); + server.sendSystemMessage(Component.literal(message)); + Component txt = c("gi "+message); + for (Player entityplayer : server.getPlayerList().getPlayers()) + { + entityplayer.sendSystemMessage(txt); + } + } + public static void print_server_message(MinecraftServer server, Component message) + { + if (server == null) + LOG.error("Message not delivered: "+message.getString()); + server.sendSystemMessage(message); + for (Player entityplayer : server.getPlayerList().getPlayers()) + { + entityplayer.sendSystemMessage(message); + } + } +} + diff --git a/src/main/java/carpet/utils/MobAI.java b/src/main/java/carpet/utils/MobAI.java new file mode 100644 index 0000000..454d234 --- /dev/null +++ b/src/main/java/carpet/utils/MobAI.java @@ -0,0 +1,89 @@ +package carpet.utils; + +import carpet.CarpetServer; +import com.google.common.collect.Sets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.Vec3; + +public class MobAI +{ + private static Map, Set> aiTrackers = new HashMap<>(); + + public static void resetTrackers() + { + aiTrackers.clear(); + } + + public static boolean isTracking(Entity e, TrackingType type) + { + if (e.getCommandSenderWorld().isClientSide()) + return false; + Set currentTrackers = aiTrackers.get(e.getType()); + if (currentTrackers == null) + return false; + return currentTrackers.contains(type); + } + + public static void clearTracking(final MinecraftServer server, EntityType etype) + { + aiTrackers.remove(etype); + for(ServerLevel world : server.getAllLevels() ) + { + for (Entity e: world.getEntities(etype, Entity::hasCustomName)) + { + e.setCustomNameVisible(false); + e.setCustomName(null); + } + } + } + + public static void startTracking(EntityType e, TrackingType type) + { + aiTrackers.putIfAbsent(e,Sets.newHashSet()); + aiTrackers.get(e).add(type); + } + + public static Stream availbleTypes(CommandSourceStack source) + { + Set> types = new HashSet<>(); + for (TrackingType type: TrackingType.values()) + { + types.addAll(type.types); + } + return types.stream().map(t -> source.registryAccess().registryOrThrow(Registries.ENTITY_TYPE).getKey(t).getPath()); + } + + public static Stream availableFor(EntityType entityType) + { + Set availableOptions = new HashSet<>(); + for (TrackingType type: TrackingType.values()) + if (type.types.contains(entityType)) + availableOptions.add(type); + return availableOptions.stream().map(t -> t.name().toLowerCase()); + } + + public enum TrackingType + { + IRON_GOLEM_SPAWNING(Set.of(EntityType.VILLAGER)), + BREEDING(Set.of(EntityType.VILLAGER)); + public final Set> types; + TrackingType(Set> applicableTypes) + { + types = applicableTypes; + } + } +} diff --git a/src/main/java/carpet/utils/PerimeterDiagnostics.java b/src/main/java/carpet/utils/PerimeterDiagnostics.java new file mode 100644 index 0000000..0b7be1b --- /dev/null +++ b/src/main/java/carpet/utils/PerimeterDiagnostics.java @@ -0,0 +1,188 @@ +package carpet.utils; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.FluidTags; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.entity.ambient.AmbientCreature; +import net.minecraft.world.entity.animal.WaterAnimal; +import net.minecraft.world.entity.monster.Enemy; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +public class PerimeterDiagnostics +{ + public static class Result + { + public int liquid; + public int ground; + public int specific; + public List samples; + Result() + { + samples = new ArrayList<>(); + } + } + private MobSpawnSettings.SpawnerData sle; + private ServerLevel worldServer; + private MobCategory ctype; + private Mob el; + private PerimeterDiagnostics(ServerLevel server, MobCategory ctype, Mob el) + { + this.sle = null; + this.worldServer = server; + this.ctype = ctype; + this.el = el; + } + + public static Result countSpots(ServerLevel worldserver, BlockPos epos, Mob el) + { + BlockPos pos; + //List samples = new ArrayList(); + //if (el != null) CarpetSettings.LOG.error(String.format("Got %s to check",el.toString())); + int eY = epos.getY(); + int eX = epos.getX(); + int eZ = epos.getZ(); + Result result = new Result(); + + //int ground_spawns = 0; + //int liquid_spawns = 0; + //int specific_spawns = 0; + boolean add_water = false; + boolean add_ground = false; + MobCategory ctype = null; + + if (el != null) + { + if (el instanceof WaterAnimal) + { + add_water = true; + ctype = MobCategory.WATER_CREATURE; + } + else if (el instanceof AgeableMob) + { + add_ground = true; + ctype = MobCategory.CREATURE; + } + else if (el instanceof Enemy) + { + add_ground = true; + ctype = MobCategory.MONSTER; + } + else if (el instanceof AmbientCreature) + { + ctype = MobCategory.AMBIENT; + } + } + PerimeterDiagnostics diagnostic = new PerimeterDiagnostics(worldserver,ctype,el); + EntityType type = EntityType.ZOMBIE; + if (el != null) type = el.getType(); + int minY = worldserver.getMinBuildHeight(); + int maxY = worldserver.getMaxBuildHeight(); + for (int x = -128; x <= 128; ++x) + { + for (int z = -128; z <= 128; ++z) + { + if (x*x + z*z > 128*128) // cut out a cyllinder first + { + continue; + } + for (int y= minY; y < maxY; ++y) + { + if ((Math.abs(y-eY)>128) ) + { + continue; + } + int distsq = (x)*(x)+(eY-y)*(eY-y)+(z)*(z); + if (distsq > 128*128 || distsq < 24*24) + { + continue; + } + pos = new BlockPos(eX+x, y, eZ+z); + + BlockState iblockstate = worldserver.getBlockState(pos); + BlockState iblockstate_down = worldserver.getBlockState(pos.below()); + BlockState iblockstate_up = worldserver.getBlockState(pos.above()); + + if ( iblockstate.getFluidState().is(FluidTags.WATER) && !iblockstate_up.isRedstoneConductor(worldserver, pos)) // isSimpleFUllBLock + { + result.liquid++; + if (add_water && diagnostic.check_entity_spawn(pos)) + { + result.specific++; + if (result.samples.size() < 10) + { + result.samples.add(pos); + } + } + } + else + { + if (iblockstate_down.isRedstoneConductor(worldserver, pos)) // isSimpleFUllBLock + { + Block block = iblockstate_down.getBlock(); + boolean flag = block != Blocks.BEDROCK && block != Blocks.BARRIER; + if( flag && NaturalSpawner.isValidEmptySpawnBlock(worldserver, pos, iblockstate, iblockstate.getFluidState(), type) && NaturalSpawner.isValidEmptySpawnBlock(worldserver, pos.above(), iblockstate_up, iblockstate_up.getFluidState(), type)) + { + result.ground ++; + if (add_ground && diagnostic.check_entity_spawn(pos)) + { + result.specific++; + if (result.samples.size() < 10) + { + result.samples.add(pos); + } + } + } + } + } + } + } + } + //ashMap result= new HashMap<>(); + //result.put("Potential in-water spawning spaces", liquid_spawns); + //result.put("Potential on-ground spawning spaces", ground_spawns); + //if (el != null) result.put(String.format("%s spawning spaces",el.getDisplayName().getUnformattedText()),specific_spawns); + return result; + } + + + private boolean check_entity_spawn(BlockPos pos) + { + if (sle == null || !worldServer.getChunkSource().getGenerator().getMobsAt(worldServer.getBiome(pos), worldServer.structureManager(), ctype, pos).unwrap().contains(sle)) + { + sle = null; + for (MobSpawnSettings.SpawnerData sle: worldServer.getChunkSource().getGenerator().getMobsAt(worldServer.getBiome(pos), worldServer.structureManager(), ctype, pos).unwrap()) + { + if (el.getType() == sle.type) + { + this.sle = sle; + break; + } + } + if (sle == null || !worldServer.getChunkSource().getGenerator().getMobsAt(worldServer.getBiome(pos), worldServer.structureManager(), ctype, pos).unwrap().contains(sle)) + { + return false; + } + } + + if (SpawnPlacements.isSpawnPositionOk(sle.type, worldServer, pos)) + { + el.moveTo(pos.getX() + 0.5F, pos.getY(), pos.getZ()+0.5F, 0.0F, 0.0F); + return el.checkSpawnObstruction(worldServer) && el.checkSpawnRules(worldServer, MobSpawnType.NATURAL) && + SpawnPlacements.checkSpawnRules(el.getType(),(ServerLevel)el.getCommandSenderWorld(), MobSpawnType.NATURAL, el.blockPosition(), el.getCommandSenderWorld().random) && + worldServer.noCollision(el); // check collision rules once they stop fiddling with them after 1.14.1 + } + return false; + } +} diff --git a/src/main/java/carpet/utils/SpawnOverrides.java b/src/main/java/carpet/utils/SpawnOverrides.java new file mode 100644 index 0000000..575b491 --- /dev/null +++ b/src/main/java/carpet/utils/SpawnOverrides.java @@ -0,0 +1,99 @@ +package carpet.utils; + +import carpet.CarpetSettings; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.BuiltinStructures; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureSpawnOverride; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.StructureType; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BooleanSupplier; + +public class SpawnOverrides { + final private static Map>, Pair> carpetOverrides = new HashMap<>(); + + static { + addOverride(() -> CarpetSettings.huskSpawningInTemples, MobCategory.MONSTER, BuiltinStructures.DESERT_PYRAMID, StructureSpawnOverride.BoundingBoxType.STRUCTURE, + WeightedRandomList.create(new MobSpawnSettings.SpawnerData(EntityType.HUSK, 1, 1, 1)) + ); + addOverride(() -> CarpetSettings.shulkerSpawningInEndCities, MobCategory.MONSTER, BuiltinStructures.END_CITY, StructureSpawnOverride.BoundingBoxType.PIECE, + WeightedRandomList.create(new MobSpawnSettings.SpawnerData(EntityType.SHULKER, 10, 4, 4)) + ); + addOverride(() -> CarpetSettings.piglinsSpawningInBastions, MobCategory.MONSTER, BuiltinStructures.BASTION_REMNANT, StructureSpawnOverride.BoundingBoxType.PIECE, + WeightedRandomList.create( + new MobSpawnSettings.SpawnerData(EntityType.PIGLIN_BRUTE, 5, 1, 2), + new MobSpawnSettings.SpawnerData(EntityType.PIGLIN, 10, 2, 4), + new MobSpawnSettings.SpawnerData(EntityType.HOGLIN, 2, 1, 2) + ) + ); + + } + + public static void addOverride(BooleanSupplier when, MobCategory cat, ResourceKey poo, + StructureSpawnOverride.BoundingBoxType type, WeightedRandomList spawns) { + carpetOverrides.put(Pair.of(cat, poo), Pair.of(when, new StructureSpawnOverride(type, spawns))); + } + + public static WeightedRandomList test(StructureManager structureFeatureManager, LongSet foo, + MobCategory cat, Structure confExisting, BlockPos where) { + ResourceLocation resource = structureFeatureManager.registryAccess().registryOrThrow(Registries.STRUCTURE).getKey(confExisting); + ResourceKey key = ResourceKey.create(Registries.STRUCTURE, resource); + final Pair spawnData = carpetOverrides.get(Pair.of(cat, key)); + if (spawnData == null || !spawnData.getKey().getAsBoolean()) return null; + StructureSpawnOverride override = spawnData.getRight(); + if (override.boundingBox() == StructureSpawnOverride.BoundingBoxType.STRUCTURE) { + if (structureFeatureManager.getStructureAt(where, confExisting).isValid()) + return override.spawns(); + } else { + List starts = new ArrayList<>(1); + structureFeatureManager.fillStartsForStructure(confExisting, foo, starts::add); + for (StructureStart start : starts) { + if (start != null && start.isValid() && structureFeatureManager.structureHasPieceAt(where, start)) { + return override.spawns(); + } + } + } + return null; + } + + public static boolean isStructureAtPosition(ServerLevel level, ResourceKey structureKey, BlockPos pos) + { + final Structure fortressFeature = level.registryAccess().registryOrThrow(Registries.STRUCTURE).get(structureKey); + if (fortressFeature == null) { + return false; + } + return level.structureManager().getStructureAt(pos, fortressFeature).isValid(); + } + + public static List startsForFeature(ServerLevel level, SectionPos sectionPos, StructureType structure) { + Map allrefs = level.getChunk(sectionPos.x(), sectionPos.z(), ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); + List result = new ArrayList<>(); + for (var entry: allrefs.entrySet()) + { + Structure existing = entry.getKey(); + if (existing.type() == structure) + { + level.structureManager().fillStartsForStructure(existing, entry.getValue(), result::add); + } + } + return result; + } +} diff --git a/src/main/java/carpet/utils/SpawnReporter.java b/src/main/java/carpet/utils/SpawnReporter.java new file mode 100644 index 0000000..512431c --- /dev/null +++ b/src/main/java/carpet/utils/SpawnReporter.java @@ -0,0 +1,505 @@ +package carpet.utils; + +import carpet.CarpetSettings; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.MobSpawnType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.entity.animal.Ocelot; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.StructureManager; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.structures.NetherFortressStructure; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; + +import static net.minecraft.world.entity.MobCategory.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +public class SpawnReporter +{ + private static final MobCategory[] CACHED_MOBCATEGORY_VALUES = MobCategory.values(); + public static boolean mockSpawns = false; + + public static final HashMap, Integer> chunkCounts = new HashMap<>(); + + public static final HashMap, MobCategory>, Object2LongOpenHashMap>> spawn_stats = new HashMap<>(); + public static double mobcap_exponent = 0.0D; + + public static final Object2LongOpenHashMap, MobCategory>> spawn_attempts = new Object2LongOpenHashMap<>(); + public static final Object2LongOpenHashMap, MobCategory>> overall_spawn_ticks = new Object2LongOpenHashMap<>(); + public static final Object2LongOpenHashMap, MobCategory>> spawn_ticks_full = new Object2LongOpenHashMap<>(); + public static final Object2LongOpenHashMap, MobCategory>> spawn_ticks_fail = new Object2LongOpenHashMap<>(); + public static final Object2LongOpenHashMap, MobCategory>> spawn_ticks_succ = new Object2LongOpenHashMap<>(); + public static final Object2LongOpenHashMap, MobCategory>> spawn_ticks_spawns = new Object2LongOpenHashMap<>(); + public static final Object2LongOpenHashMap, MobCategory>> spawn_cap_count = new Object2LongOpenHashMap<>(); + public static final HashMap, MobCategory>, EvictingQueue, BlockPos>>> spawned_mobs = new HashMap<>(); + public static final HashMap spawn_tries = new HashMap<>(); + + private static int spawnTrackingStartTime = 0; + private static BoundingBox trackedSpawningArea = null; + // in case game gets each thread for each world - these need to belong to workd. + public static Object2LongOpenHashMap local_spawns = null; // per world + public static HashSet first_chunk_marker = null; + + public static void registerSpawn(Mob mob, MobCategory cat, BlockPos pos) + { + if (trackedSpawningArea != null && !trackedSpawningArea.isInside(pos)) + { + return; + } + Pair, MobCategory> key = Pair.of(mob.level().dimension(), cat); + spawn_stats.get(key).addTo(mob.getType(), 1); + spawned_mobs.get(key).put(Pair.of(mob.getType(), pos)); + if (!local_spawns.containsKey(cat)) + { + CarpetSettings.LOG.error("Rogue spawn detected for category "+cat.getName()+" for mob "+mob.getType().getDescription().getString()+". If you see this message let carpet peeps know about it on github issues."); + local_spawns.put(cat, 0L); + } + local_spawns.addTo(cat, 1); + } + + public static final int MAGIC_NUMBER = (int)Math.pow(17.0D, 2.0D); + /*public static double currentMagicNumber() + { + return MAGIC_NUMBER / (Math.pow(2.0,(SpawnReporter.mobcap_exponent/4))); + }*/ + + public static List printMobcapsForDimension(ServerLevel world, boolean multiline) + { + ResourceKey dim = world.dimension(); + String name = dim.location().getPath(); + List lst = new ArrayList<>(); + if (multiline) + lst.add(Messenger.s(String.format("Mobcaps for %s:",name))); + NaturalSpawner.SpawnState lastSpawner = world.getChunkSource().getLastSpawnState(); + Object2IntMap dimCounts = lastSpawner.getMobCategoryCounts(); + int chunkcount = chunkCounts.getOrDefault(dim, -1); + if (dimCounts == null || chunkcount < 0) + { + lst.add(Messenger.c("g --UNAVAILABLE--")); + return lst; + } + + List shortCodes = new ArrayList<>(); + for (MobCategory category : cachedMobCategories()) + { + int cur = dimCounts.getOrDefault(category, -1); + int max = (int)(chunkcount * ((double)category.getMaxInstancesPerChunk() / MAGIC_NUMBER)); // from ServerChunkManager.CHUNKS_ELIGIBLE_FOR_SPAWNING + String color = Messenger.heatmap_color(cur, max); + String mobColor = Messenger.creatureTypeColor(category); + if (multiline) + { + int rounds = spawn_tries.get(category); + lst.add(Messenger.c(String.format("w %s: ", category.getName()), + (cur < 0) ? "g -" : (color + " " + cur), "g / ", mobColor + " " + max, + (rounds == 1) ? "w " : String.format("gi (%d rounds/tick)", spawn_tries.get(category)) + )); + } + else + { + shortCodes.add(color+" "+((cur<0)?"-":cur)); + shortCodes.add("g /"); + shortCodes.add(mobColor+" "+max); + shortCodes.add("g ,"); + } + } + if (!multiline) + { + if (shortCodes.size() > 0) + { + shortCodes.remove(shortCodes.size() - 1); + lst.add(Messenger.c(shortCodes.toArray(new Object[0]))); + } + else + { + lst.add(Messenger.c("g --UNAVAILABLE--")); + } + + } + return lst; + } + + public static List getRecentSpawns(Level world, MobCategory category) + { + List lst = new ArrayList<>(); + if (!trackingSpawns()) + { + lst.add(Messenger.s("Spawn tracking not started")); + return lst; + } + String categoryName = category.getName(); + + lst.add(Messenger.s(String.format("Recent %s spawns:", categoryName))); + for (Pair, BlockPos> pair : spawned_mobs.get(Pair.of(world.dimension(), category)).keySet()) + { + lst.add( Messenger.c( + "w - ", + Messenger.tp("wb",pair.getRight()), + String.format("w : %s", pair.getLeft().getDescription().getString()) + )); + } + + if (lst.size() == 1) + { + lst.add(Messenger.s(" - Nothing spawned yet, sorry.")); + } + return lst; + + } + + public static List handleWoolAction(BlockPos pos, ServerLevel worldIn) + { + DyeColor under = WoolTool.getWoolColorAtPosition(worldIn, pos.below()); + if (under == null) + { + if (trackingSpawns()) + { + return makeTrackingReport(worldIn); + } + else + { + return printMobcapsForDimension(worldIn, true ); + } + } + MobCategory category = getCategoryFromWoolColor(under); + if (category != null) + { + if (trackingSpawns()) + { + return getRecentSpawns(worldIn, category); + } + else + { + return printEntitiesByType(category, worldIn, true); + + } + + } + if (trackingSpawns()) + { + return makeTrackingReport(worldIn); + } + else + { + return printMobcapsForDimension(worldIn, true ); + } + + } + + public static MobCategory getCategoryFromWoolColor(DyeColor color) + { + return switch (color) + { + case RED -> MONSTER; + case GREEN -> CREATURE; + case BLUE -> WATER_CREATURE; + case BROWN -> AMBIENT; + case CYAN -> WATER_AMBIENT; + default -> null; + }; + } + + public static List printEntitiesByType(MobCategory cat, ServerLevel worldIn, boolean all) + { + List lst = new ArrayList<>(); + lst.add( Messenger.s(String.format("Loaded entities for %s category:", cat))); + for (Entity entity : worldIn.getEntities(EntityTypeTest.forClass(Entity.class), (e) -> e.getType().getCategory() == cat)) + { + boolean persistent = entity instanceof Mob mob && ( mob.isPersistenceRequired() || mob.requiresCustomPersistence()); + if (!all && persistent) + continue; + + EntityType type = entity.getType(); + BlockPos pos = entity.blockPosition(); + lst.add( Messenger.c( + "w - ", + Messenger.tp(persistent ? "gb" : "wb", pos), + String.format(persistent ? "g : %s" : "w : %s", type.getDescription().getString()) + )); + + } + if (lst.size() == 1) + { + lst.add(Messenger.s(" - Empty.")); + } + return lst; + } + + public static void initializeMocking() + { + mockSpawns = true; + } + public static void stopMocking() + { + mockSpawns = false; + } + + public static void resetSpawnStats(MinecraftServer server, boolean full) + { + if (full) + { + for (MobCategory category : cachedMobCategories()) + spawn_tries.put(category, 1); + } + overall_spawn_ticks.clear(); + spawn_attempts.clear(); + spawn_ticks_full.clear(); + spawn_ticks_fail.clear(); + spawn_ticks_succ.clear(); + spawn_ticks_spawns.clear(); + spawn_cap_count.clear(); + + // can't fast-path to clear given different worlds could have different amount of worlds + for (MobCategory category : cachedMobCategories()) { + for (ResourceKey world : server.levelKeys()) { + Pair, MobCategory> key = Pair.of(world, category); + spawn_stats.put(key, new Object2LongOpenHashMap<>()); + spawned_mobs.put(key, new EvictingQueue<>()); + } + } + spawnTrackingStartTime = 0; + } + + public static MobCategory[] cachedMobCategories() { + return CACHED_MOBCATEGORY_VALUES; + } + + public static boolean trackingSpawns() { + return spawnTrackingStartTime != 0L; + } + + public static void startTracking(MinecraftServer server, BoundingBox trackedArea) { + resetSpawnStats(server, false); + spawnTrackingStartTime = server.getTickCount(); + trackedSpawningArea = trackedArea; + } + + public static void stopTracking(MinecraftServer server) { + resetSpawnStats(server, false); + SpawnReporter.spawnTrackingStartTime = 0; + trackedSpawningArea = null; + } + + private static String getWorldCode(ResourceKey world) + { + if (world == Level.OVERWORLD) return ""; + return "("+Character.toUpperCase(world.location().getPath().charAt("THE_".length()))+")"; + } + + public static List makeTrackingReport(Level worldIn) + { + List report = new ArrayList<>(); + if (!trackingSpawns()) + { + report.add(Messenger.c( + "w Spawn tracking is disabled, type '", + "wi /spawn tracking start","/spawn tracking start", + "w ' to enable")); + return report; + } + int duration = worldIn.getServer().getTickCount() - spawnTrackingStartTime; + report.add(Messenger.c("bw --------------------")); + String simulated = mockSpawns ? "[SIMULATED] " : ""; + String location = (trackedSpawningArea != null) ? String.format("[in (%d, %d, %d)x(%d, %d, %d)]", + trackedSpawningArea.minX(), trackedSpawningArea.minY(), trackedSpawningArea.minZ(), + trackedSpawningArea.maxX(), trackedSpawningArea.maxY(), trackedSpawningArea.maxZ() ):""; + report.add(Messenger.s(String.format("%sSpawn statistics %s: for %.1f min", simulated, location, (duration/72000.0)*60))); + + for (MobCategory category : cachedMobCategories()) + { + for (ResourceKey dim : worldIn.getServer().levelKeys()) + { + Pair, MobCategory> key = Pair.of(dim, category); + if (spawn_ticks_spawns.getLong(key) > 0L) + { + double hours = overall_spawn_ticks.getLong(key)/72000.0; + long spawnAttemptsForCategory = spawn_attempts.getLong(key); + report.add(Messenger.s(String.format(" > %s%s (%.1f min), %.1f m/t, %%{%.1fF %.1f- %.1f+}; %.2f s/att", + category.getName().substring(0,3), getWorldCode(dim), + 60*hours, + (1.0D * spawn_cap_count.getLong(key)) / spawnAttemptsForCategory, + (100.0D * spawn_ticks_full.getLong(key)) / spawnAttemptsForCategory, + (100.0D * spawn_ticks_fail.getLong(key)) / spawnAttemptsForCategory, + (100.0D * spawn_ticks_succ.getLong(key)) / spawnAttemptsForCategory, + (1.0D * spawn_ticks_spawns.getLong(key)) / (spawn_ticks_fail.getLong(key) + spawn_ticks_succ.getLong(key)) + ))); + for (Object2LongMap.Entry> entry: spawn_stats.get(key).object2LongEntrySet()) + { + report.add(Messenger.s(String.format(" - %s: %d spawns, %d per hour", + entry.getKey().getDescription().getString(), + entry.getLongValue(), + (72000 * entry.getLongValue()/duration )))); + } + } + } + } + return report; + } + + public static void killEntity(LivingEntity entity) + { + if (entity.isPassenger()) + { + entity.getVehicle().discard(); + } + if (entity.isVehicle()) + { + for (Entity e: entity.getPassengers()) + { + e.discard(); + } + } + if (entity instanceof Ocelot) + { + for (Entity e: entity.getCommandSenderWorld().getEntities(entity, entity.getBoundingBox())) + { + e.discard(); + } + } + entity.discard(); + } + + // yeeted from NaturalSpawner - temporary access fix + private static List getSpawnEntries(ServerLevel serverLevel, StructureManager structureManager, ChunkGenerator chunkGenerator, MobCategory mobCategory, BlockPos blockPos, @Nullable Holder holder) { + return NaturalSpawner.isInNetherFortressBounds(blockPos, serverLevel, mobCategory, structureManager) ? NetherFortressStructure.FORTRESS_ENEMIES.unwrap() : chunkGenerator.getMobsAt(holder != null ? holder : serverLevel.getBiome(blockPos), structureManager, mobCategory, blockPos).unwrap(); + } + + public static List report(BlockPos pos, ServerLevel worldIn) + { + List rep = new ArrayList<>(); + int x = pos.getX(); + int y = pos.getY(); + int z = pos.getZ(); + ChunkAccess chunk = worldIn.getChunk(pos); + int lc = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) + 1; + String relativeHeight = (y == lc) ? "right at it." : String.format("%d blocks %s it.", Mth.abs(y - lc), (y >= lc) ? "above" : "below"); + rep.add(Messenger.s(String.format("Maximum spawn Y value for (%+d, %+d) is %d. You are " + relativeHeight, x, z, lc))); + rep.add(Messenger.s("Spawns:")); + for (MobCategory category : cachedMobCategories()) + { + String categoryCode = String.valueOf(category).substring(0, 3); + List lst = getSpawnEntries(worldIn, worldIn.structureManager(), worldIn.getChunkSource().getGenerator(), category, pos, worldIn.getBiome(pos)); + if (lst != null && !lst.isEmpty()) + { + for (MobSpawnSettings.SpawnerData spawnEntry : lst) + { + if (SpawnPlacements.getPlacementType(spawnEntry.type) == null) + continue; // vanilla bug + boolean canSpawn = SpawnPlacements.isSpawnPositionOk(spawnEntry.type, worldIn, pos); + int willSpawn = -1; + boolean fits = false; + + Mob mob; + try + { + mob = (Mob) spawnEntry.type.create(worldIn); + } + catch (Exception e) + { + CarpetSettings.LOG.warn("Exception while creating mob for spawn reporter", e); + return rep; + } + + if (canSpawn) + { + willSpawn = 0; + for (int attempt = 0; attempt < 50; ++attempt) + { + float f = x + 0.5F; + float f1 = z + 0.5F; + mob.moveTo(f, y, f1, worldIn.random.nextFloat() * 360.0F, 0.0F); + fits = worldIn.noCollision(mob); + EntityType etype = mob.getType(); + + for (int i = 0; i < 20; ++i) + { + if ( + SpawnPlacements.checkSpawnRules(etype,worldIn, MobSpawnType.NATURAL, pos, worldIn.random) && + SpawnPlacements.isSpawnPositionOk(etype, worldIn, pos) && + mob.checkSpawnRules(worldIn, MobSpawnType.NATURAL) + // && mob.canSpawn(worldIn) // entity collisions // mostly - except ocelots + ) + { + if (etype == EntityType.OCELOT) + { + BlockState blockState = worldIn.getBlockState(pos.below()); + if ((pos.getY() < worldIn.getSeaLevel()) || !(blockState.is(Blocks.GRASS_BLOCK) || blockState.is(BlockTags.LEAVES))) { + continue; + } + } + willSpawn += 1; + } + } + mob.finalizeSpawn(worldIn, worldIn.getCurrentDifficultyAt(mob.blockPosition()), MobSpawnType.NATURAL, null); + // the code invokes onInitialSpawn after getCanSpawHere + fits = fits && worldIn.noCollision(mob); + + killEntity(mob); + + try + { + mob = (Mob) spawnEntry.type.create(worldIn); + } + catch (Exception e) + { + CarpetSettings.LOG.warn("Exception while creating mob for spawn reporter", e); + return rep; + } + } + } + + String mobTypeName = mob.getType().getDescription().getString(); + //String pack_size = Integer.toString(mob.getMaxSpawnClusterSize());//String.format("%d-%d", animal.minGroupCount, animal.maxGroupCount); + int weight = spawnEntry.getWeight().asInt(); + if (canSpawn) + { + String color = (fits && willSpawn > 0) ? "e" : "gi"; + rep.add(Messenger.c( + String.format("%s %s: %s (%d:%d-%d/%d), can: ", color, categoryCode, mobTypeName, weight, spawnEntry.minCount, spawnEntry.maxCount, mob.getMaxSpawnClusterSize()), + "l YES", + color + " , fit: ", + (fits ? "l YES" : "r NO"), + color + " , will: ", + ((willSpawn > 0)?"l ":"r ") + Math.round((double)willSpawn) / 10 + "%" + )); + } + else + { + rep.add(Messenger.c(String.format("gi %s: %s (%d:%d-%d/%d), can: ", categoryCode, mobTypeName, weight, spawnEntry.minCount, spawnEntry.maxCount, mob.getMaxSpawnClusterSize()), "n NO")); + } + killEntity(mob); + } + } + } + return rep; + } +} diff --git a/src/main/java/carpet/utils/TranslationKeys.java b/src/main/java/carpet/utils/TranslationKeys.java new file mode 100644 index 0000000..b1b9466 --- /dev/null +++ b/src/main/java/carpet/utils/TranslationKeys.java @@ -0,0 +1,31 @@ +package carpet.utils; + +/** + * This is not public API! + */ +public final class TranslationKeys { + public static final String BASE_RULE_NAMESPACE = "%s.rule."; + public static final String BASE_RULE_PATTERN = BASE_RULE_NAMESPACE + "%s."; // [settingsManager].rule.[name] + public static final String RULE_NAME_PATTERN = BASE_RULE_PATTERN + "name"; + public static final String RULE_DESC_PATTERN = BASE_RULE_PATTERN + "desc"; + public static final String RULE_EXTRA_PREFIX_PATTERN = BASE_RULE_PATTERN + "extra."; + public static final String CATEGORY_PATTERN = "%s.category.%s"; //[settingsManager].category.[name] + + // Settings command + private static final String SETTINGS_BASE = "carpet.settings.command."; + public static final String BROWSE_CATEGORIES = SETTINGS_BASE + "browse_categories"; + public static final String VERSION = SETTINGS_BASE + "version"; + public static final String LIST_ALL_CATEGORY = SETTINGS_BASE + "list_all_category"; + public static final String CURRENT_SETTINGS_HEADER = SETTINGS_BASE + "current_settings_header"; + public static final String SWITCH_TO = SETTINGS_BASE + "switch_to"; + public static final String UNKNOWN_RULE = SETTINGS_BASE + "unknown_rule"; + public static final String CURRENT_FROM_FILE_HEADER = SETTINGS_BASE + "current_from_file_header"; + public static final String MOD_SETTINGS_MATCHING = SETTINGS_BASE + "mod_settings_matching"; + public static final String ALL_MOD_SETTINGS = SETTINGS_BASE + "all_mod_settings"; + public static final String TAGS = SETTINGS_BASE + "tags"; + public static final String CHANGE_PERMANENTLY = SETTINGS_BASE + "change_permanently"; + public static final String CHANGE_PERMANENTLY_HOVER = SETTINGS_BASE + "change_permanently_tooltip"; + public static final String DEFAULT_SET = SETTINGS_BASE + "default_set"; + public static final String DEFAULT_REMOVED = SETTINGS_BASE + "default_removed"; + public static final String CURRENT_VALUE = SETTINGS_BASE + "current_value"; +} diff --git a/src/main/java/carpet/utils/Translations.java b/src/main/java/carpet/utils/Translations.java new file mode 100644 index 0000000..d69e857 --- /dev/null +++ b/src/main/java/carpet/utils/Translations.java @@ -0,0 +1,126 @@ +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 java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class Translations +{ + private static Map translationMap = Collections.emptyMap(); + + public static String tr(String key) + { + return translationMap.getOrDefault(key, key); + } + + public static String trOrNull(String key) + { + return translationMap.get(key); + } + + public static String tr(String key, String str) + { + return translationMap.getOrDefault(key, str); + } + + public static boolean hasTranslations() + { + return !translationMap.isEmpty(); + } + + public static boolean hasTranslation(String key) + { + return translationMap.containsKey(key); + } + + public static Map getTranslationFromResourcePath(String path) + { + InputStream langFile = Translations.class.getClassLoader().getResourceAsStream(path); + if (langFile == null) { + // we don't have that language + return Collections.emptyMap(); + } + Gson gson = new GsonBuilder().setLenient().create(); + return gson.fromJson(new InputStreamReader(langFile, StandardCharsets.UTF_8), + new TypeToken>() {}); + } + + public static void updateLanguage() + { + Map translations = new HashMap<>(); + translations.putAll(getTranslationFromResourcePath(String.format("assets/carpet/lang/%s.json", CarpetSettings.language))); + + for (CarpetExtension ext : CarpetServer.extensions) + { + Map extMappings = ext.canHasTranslations(CarpetSettings.language); + if (extMappings == null) continue; // would be nice to get rid of this, but too many extensions return null where they don't know they do + boolean warned = false; + for (var entry : extMappings.entrySet()) { + var key = entry.getKey(); + // Migrate the old format + if (!key.startsWith("carpet.")) { + if (key.startsWith("rule.")) { + // default to carpet's settings manager. Custom managers are really uncommon and the known ones don't provide translations anyway + key = TranslationKeys.BASE_RULE_NAMESPACE.formatted("carpet") + key.substring(5); + } else if (key.startsWith("category.")) { + key = TranslationKeys.CATEGORY_PATTERN.formatted("carpet", key.substring(9)); + } + if (!warned && key != entry.getKey()) { + CarpetSettings.LOG.warn(""" + Found outdated translation keys in extension '%s'! + These won't be supported in a later Carpet version! + Carpet will now try to map them to the correct keys in a best-effort basis""".formatted(ext.getClass().getName())); + warned = true; + } + } + translations.putIfAbsent(key, entry.getValue()); + } + } + translations.keySet().removeIf(e -> { + if (e.startsWith("//")) { + CarpetSettings.LOG.warn(""" + Found translation key starting with // while preparing translations! + Doing this is deprecated and may cause issues in later versions! Consider settings GSON to "lenient" mode and + using regular comments instead! + Translation key is '%s'""".formatted(e)); + return true; + } else + return false; + }); + // Remove after deprecated settings api is removed + addFallbacksTo(translations); + translationMap = translations; + } + + public static boolean isValidLanguage(String newValue) + { + // will put some validations for availble languages at some point + return true; + } + + // fallbacks for old rules that don't define rule descriptions or stuff in language files yet + // to be removed when old settings system is removed and translation refactor is finished + + private static final Map FALLBACKS = new HashMap<>(); + /** + * @deprecated if you compile against this method I'll steal your kneecaps + */ + @Deprecated(forRemoval = true) + public static void registerFallbackTranslation(String key, String description) { + FALLBACKS.put(key, description); + } + + private static void addFallbacksTo(Map translationMap) { + FALLBACKS.forEach(translationMap::putIfAbsent); + } +} diff --git a/src/main/java/carpet/utils/WoolTool.java b/src/main/java/carpet/utils/WoolTool.java new file mode 100644 index 0000000..7d3fa7f --- /dev/null +++ b/src/main/java/carpet/utils/WoolTool.java @@ -0,0 +1,129 @@ +package carpet.utils; + +import carpet.CarpetSettings; +import carpet.helpers.HopperCounter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.MapColor; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +import static java.util.Map.entry; + +/** + * A series of utility functions and variables for dealing predominantly with hopper counters and determining which counter + * to add their items to, as well as helping dealing with carpet functionality. + */ +public class WoolTool +{ + /** + * A map from a wool {@link Block} to its {@link DyeColor} which is used in {@link WoolTool#getWoolColorAtPosition} + * to get the colour of wool at a position. + */ + private static final Map WOOL_BLOCK_TO_DYE = Map.ofEntries( + entry(Blocks.WHITE_WOOL, DyeColor.WHITE), + entry(Blocks.ORANGE_WOOL, DyeColor.ORANGE), + entry(Blocks.MAGENTA_WOOL, DyeColor.MAGENTA), + entry(Blocks.LIGHT_BLUE_WOOL, DyeColor.LIGHT_BLUE), + entry(Blocks.YELLOW_WOOL, DyeColor.YELLOW), + entry(Blocks.LIME_WOOL, DyeColor.LIME), + entry(Blocks.PINK_WOOL, DyeColor.PINK), + entry(Blocks.GRAY_WOOL, DyeColor.GRAY), + entry(Blocks.LIGHT_GRAY_WOOL, DyeColor.LIGHT_GRAY), + entry(Blocks.CYAN_WOOL, DyeColor.CYAN), + entry(Blocks.PURPLE_WOOL, DyeColor.PURPLE), + entry(Blocks.BLUE_WOOL, DyeColor.BLUE), + entry(Blocks.BROWN_WOOL, DyeColor.BROWN), + entry(Blocks.GREEN_WOOL, DyeColor.GREEN), + entry(Blocks.RED_WOOL, DyeColor.RED), + entry(Blocks.BLACK_WOOL, DyeColor.BLACK) + ); + + /** + * The method which gets triggered when a player places a carpet, and decides what to do based on the carpet's colour: + *
    + *
  • Red - Resets the counter of the colour of wool underneath the carpet (if there is no wool, then nothing happens)
  • + *
  • Green - Prints the contents of the counter of the colour of wool underneath the carpet
  • + *
+ */ + public static void carpetPlacedAction(DyeColor color, Player placer, BlockPos pos, ServerLevel worldIn) + { + if (!CarpetSettings.carpets) + { + return; + } + switch (color) + { + case PINK: + if (!"false".equals(CarpetSettings.commandSpawn)) + Messenger.send(placer, SpawnReporter.report(pos, worldIn)); + + break; + case BLACK: + if (!"false".equals(CarpetSettings.commandSpawn)) + Messenger.send(placer, SpawnReporter.handleWoolAction(pos, worldIn)); + break; + case BROWN: + if (!"false".equals(CarpetSettings.commandDistance)) + { + CommandSourceStack source = placer.createCommandSourceStack(); + if (!DistanceCalculator.hasStartingPoint(source) || placer.isShiftKeyDown()) { + DistanceCalculator.setStart(source, Vec3.atLowerCornerOf(pos) ); // zero padded pos + } + else { + DistanceCalculator.setEnd(source, Vec3.atLowerCornerOf(pos)); + } + } + break; + case GRAY: + if (!"false".equals(CarpetSettings.commandInfo)) + Messenger.send(placer, BlockInfo.blockInfo(pos.below(), worldIn)); + break; + case GREEN: + if (CarpetSettings.hopperCounters) + { + DyeColor under = getWoolColorAtPosition(worldIn, pos.below()); + if (under == null) return; + HopperCounter counter = HopperCounter.getCounter(under); + Messenger.send(placer, counter.format(worldIn.getServer(), false, false)); + } + break; + case RED: + if (CarpetSettings.hopperCounters) + { + DyeColor under = getWoolColorAtPosition(worldIn, pos.below()); + if (under == null) return; + HopperCounter counter = HopperCounter.getCounter(under); + counter.reset(placer.getServer()); + List res = new ArrayList<>(); + res.add(Messenger.s(String.format("%s counter reset",under.toString()))); + Messenger.send(placer, res); + } + break; + } + } + + /** + * Gets the colour of wool at the position, for hoppers to be able to decide whether to add their items to the global counter. + */ + @Nullable + public static DyeColor getWoolColorAtPosition(Level worldIn, BlockPos pos) + { + BlockState state = worldIn.getBlockState(pos); + return WOOL_BLOCK_TO_DYE.get(state.getBlock()); + } +} diff --git a/src/main/resources/assets/carpet/icon.png b/src/main/resources/assets/carpet/icon.png new file mode 100644 index 0000000..af2c48c Binary files /dev/null and b/src/main/resources/assets/carpet/icon.png differ 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 0000000..0fcbc57 --- /dev/null +++ b/src/main/resources/assets/carpet/lang/en_us.json @@ -0,0 +1,36 @@ +{ + // TODO Rules + + // Categories: + + "carpet.category.bugfix": "bugfix", + "carpet.category.survival": "survival", + "carpet.category.creative": "creative", + "carpet.category.experimental": "experimental", + "carpet.category.optimization": "optimization", + "carpet.category.feature": "feature", + "carpet.category.command": "command", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "dispenser", + "carpet.category.scarpet": "scarpet", + "carpet.category.client": "client", + + // Settings command UI elements + + "carpet.settings.command.browse_categories": "Browse categories", + "carpet.settings.command.version": "version", + "carpet.settings.command.list_all_category": "list all %s settings", + "carpet.settings.command.current_settings_header": "Current %s settings", + "carpet.settings.command.switch_to": "Switch to %s", + "carpet.settings.command.unknown_rule": "Unknown rule", + "carpet.settings.command.current_from_file_header": "Current %s Startup Settings from %s", + "carpet.settings.command.mod_settings_matching": "%s settings matching \"%s\" ", + "carpet.settings.command.all_mod_settings": "All %s settings", + "carpet.settings.command.tags": "Tags", + "carpet.settings.command.change_permanently": "Change permanently", + "carpet.settings.command.change_permanently_tooltip": "click to keep the settings in %s to save across restarts", + "carpet.settings.command.default_set": "Rule %s will now default to %s", + "carpet.settings.command.default_removed": "Rule %s will no longer be set on restart", + "carpet.settings.command.current_value": "current value" + +} diff --git a/src/main/resources/assets/carpet/lang/fr_fr.json b/src/main/resources/assets/carpet/lang/fr_fr.json new file mode 100644 index 0000000..c5d3936 --- /dev/null +++ b/src/main/resources/assets/carpet/lang/fr_fr.json @@ -0,0 +1,426 @@ +{ + // Settings command UI elements + + "carpet.settings.command.browse_categories": "Parcourir les catégories", + "carpet.settings.command.version": "version", + "carpet.settings.command.list_all_category": "liste toutes les options de la catégorie %s", + "carpet.settings.command.current_settings_header": "Options actuelles de %s", + "carpet.settings.command.switch_to": "Passer à %s", + "carpet.settings.command.unknown_rule": "Règle inconnue", + "carpet.settings.command.current_from_file_header": "Options de démarrage actuelles de %s depuis %s", + "carpet.settings.command.mod_settings_matching": "Options de %s correspondant à \"%s\" ", + "carpet.settings.command.all_mod_settings": "Toutes les options de %s", + "carpet.settings.command.tags": "Balises", + "carpet.settings.command.change_permanently": "Changer définitivement", + "carpet.settings.command.change_permanently_tooltip": "cliquez pour conserver les options dans %s et les enregistrer entre les redémarrages", + "carpet.settings.command.default_set": "La règle %s sera désormais définie par défaut sur %s", + "carpet.settings.command.default_removed": "La règle %s ne sera plus définie au redémarrage", + "carpet.settings.command.current_value": "valeur actuelle", + + // Categories: + + "carpet.category.bugfix": "correction de bugs", + "carpet.category.survival": "survie", + "carpet.category.creative": "créatif", + "carpet.category.experimental": "expérimental", + "carpet.category.optimization": "optimisation", + "carpet.category.feature": "fonctionnalité", + "carpet.category.command": "commande", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "distributeur", + "carpet.category.scarpet": "scarpet", + "carpet.category.client": "client", + + // Rules + + // allowSpawningOfflinePlayers + "carpet.rule.allowSpawningOfflinePlayers.desc": "Fait apparaître les joueurs hors ligne en mode en ligne si le joueur en online-mode avec le nom spécifié n'existe pas", + + // antiCheatDisabled + "carpet.rule.antiCheatDisabled.desc": "Empêche les joueurs de se téléporter en arrière lorsqu'ils se déplacent trop rapidement", + + "carpet.rule.antiCheatDisabled.extra.0": "... ou d'être expulsé pour 'vol'", + "carpet.rule.antiCheatDisabled.extra.1": "Fait davantage confiance au positionnement des clients", + "carpet.rule.antiCheatDisabled.extra.2": "Augmente la distance de minage autorisée du joueur à 32 blocs", + + // carpetCommandPermissionLevel + "carpet.rule.carpetCommandPermissionLevel.desc": "Niveau de permission pour les commandes Carpet. Peut être défini uniquement via le fichier .conf", + + // carpets + "carpet.rule.carpets.desc": "Placer des tapis peut entraîner l'exécution de commandes Carpet pour les joueurs non-opérateurs", + + // chainStone + "carpet.rule.chainStone.desc": "Les chaînes se colleront les unes aux autres sur les extrémités longues", + + "carpet.rule.chainStone.extra.0": "et se colleront aux autres blocs qui y sont directement connectés.", + "carpet.rule.chainStone.extra.1": "Avec stick_to_all : elles colleront même si elles ne sont pas connectées visuellement", + + // cleanLogs + "carpet.rule.cleanLogs.desc": "Supprime les messages indésirables des journaux", + + "carpet.rule.cleanLogs.extra.0": "N'affiche pas 'Maximum sound pool size 247 reached'", + "carpet.rule.cleanLogs.extra.1": "Ce qui est normal avec de bonnes fermes et mécanismes", + + // commandDistance + "carpet.rule.commandDistance.desc": "Active la commande /distance pour mesurer la distance en jeu entre deux points", + + "carpet.rule.commandDistance.extra.0": "Active également l'action de pose de tapis marron si la règle 'carpets' est également activée", + + // commandDraw + "carpet.rule.commandDraw.desc": "Active les commandes /draw", + + "carpet.rule.commandDraw.extra.0": "... permet de dessiner des formes simples ou", + "carpet.rule.commandDraw.extra.1": "d'autres formes qui sont un peu difficiles à réaliser normalement", + + // commandInfo + "carpet.rule.commandInfo.desc": "Active la commande /info pour les blocs", + + "carpet.rule.commandInfo.extra.0": "Active également l'action de pose de tapis gris", + "carpet.rule.commandInfo.extra.1": "si la règle 'carpets' est également activée", + + // commandLog + "carpet.rule.commandLog.desc": "Active la commande /log pour surveiller les événements via le chat et les superpositions", + + // commandPerimeterInfo + "carpet.rule.commandPerimeterInfo.desc": "Active la commande /perimeterinfo", + + "carpet.rule.commandPerimeterInfo.extra.0": "... qui analyse la zone autour du bloc pour trouver des emplacements potentiellement exploitables", + + // commandPlayer + "carpet.rule.commandPlayer.desc": "Active la commande /player pour contrôler/faire apparaître des joueurs", + + // commandProfile + "carpet.rule.commandProfile.desc": "Active la commande /profile pour surveiller les performances du jeu", + + "carpet.rule.commandProfile.extra.0": "sous-ensemble des capacités de la commande /tick", + + // commandScript + "carpet.rule.commandScript.desc": "Active la commande /script", + + "carpet.rule.commandScript.extra.0": "Une API de scripting en jeu pour le langage de programmation Scarpet", + + // commandScriptACE + "carpet.rule.commandScriptACE.desc": "Active les restrictions pour l'exécution de code arbitraire avec Scarpet", + + "carpet.rule.commandScriptACE.extra.0": "Les utilisateurs qui n'ont pas ce niveau de permission", + "carpet.rule.commandScriptACE.extra.1": "ne pourront pas charger d'applications ou exécuter /script run.", + "carpet.rule.commandScriptACE.extra.2": "C'est également le niveau de permission que les applications", + "carpet.rule.commandScriptACE.extra.3": "auront lors de l'exécution de commandes avec run()", + + // commandSpawn + "carpet.rule.commandSpawn.desc": "Active la commande /spawn pour le suivi des points d'apparition", + + // commandTick + "carpet.rule.commandTick.desc": "Active la commande /tick pour contrôler les horloges du jeu", + + // commandTrackAI + "carpet.rule.commandTrackAI.desc": "Permet de suivre l'IA des mobs via la commande /track", + + // creativeFlyDrag + "carpet.rule.creativeFlyDrag.desc": "Résistance de l'air en mode créatif", + + "carpet.rule.creativeFlyDrag.extra.0": "Une résistance accrue ralentira votre vol", + "carpet.rule.creativeFlyDrag.extra.1": "Vous devrez donc ajuster votre vitesse en conséquence", + "carpet.rule.creativeFlyDrag.extra.2": "Avec une résistance de 1.0, une vitesse de 11 semble correspondre aux vitesses normales de base.", + "carpet.rule.creativeFlyDrag.extra.3": "Paramètre purement côté client, ce qui signifie que", + "carpet.rule.creativeFlyDrag.extra.4": "le configurer sur le serveur dédié n'a aucun effet", + "carpet.rule.creativeFlyDrag.extra.5": "mais cela signifie également que cela fonctionnera également sur les serveurs vanilla", + + // creativeFlySpeed + "carpet.rule.creativeFlySpeed.desc": "Multiplicateur de vitesse de vol en mode créatif", + + "carpet.rule.creativeFlySpeed.extra.0": "Paramètre purement côté client, ce qui signifie que", + "carpet.rule.creativeFlySpeed.extra.1": "le configurer sur le serveur dédié n'a aucun effet", + "carpet.rule.creativeFlySpeed.extra.2": "mais cela signifie également que cela fonctionnera également sur les serveurs vanilla", + + // creativeNoClip + "carpet.rule.creativeNoClip.desc": "Mode créatif sans collision", + + "carpet.rule.creativeNoClip.extra.0": "Sur les serveurs, cela doit être configuré à la fois", + "carpet.rule.creativeNoClip.extra.1": "sur le client et sur le serveur pour fonctionner correctement.", + "carpet.rule.creativeNoClip.extra.2": "Aucun effet lorsqu'il est configuré uniquement sur le serveur", + "carpet.rule.creativeNoClip.extra.3": "Peut permettre de traverser les murs", + "carpet.rule.creativeNoClip.extra.4": "s'il est uniquement configuré côté client Carpet", + "carpet.rule.creativeNoClip.extra.5": "mais nécessite un peu de magie avec des trappes", + "carpet.rule.creativeNoClip.extra.6": "pour permettre au joueur d'entrer dans les blocs", + + // creativePlayersLoadChunks + "carpet.rule.creativePlayersLoadChunks.desc": "Les joueurs en mode créatif chargent les chunks, ou non ! Tout comme les spectateurs !", + + "carpet.rule.creativePlayersLoadChunks.extra.0": "La bascule se comporte exactement comme si le joueur était en mode spectateur et basculait le gamerule spectatorsGenerateChunks.", + + // ctrlQCraftingFix + "carpet.rule.ctrlQCraftingFix.desc": "Jeter des piles entières fonctionne également depuis l'interface de fabrication", + + // customMOTD + "carpet.rule.customMOTD.desc": "Définit un message MOTD différent lorsqu'un client essaie de se connecter au serveur", + + "carpet.rule.customMOTD.extra.0": "utilisez '_' pour utiliser le paramètre de démarrage de server.properties", + + // defaultLoggers + "carpet.rule.defaultLoggers.desc": "Définit ces loggers avec leurs configurations par défaut pour tous les nouveaux joueurs", + + "carpet.rule.defaultLoggers.extra.0": "utilisez une liste séparée par des virgules, comme 'tps,mobcaps' pour plusieurs loggers, aucun pour rien", + + // desertShrubs + "carpet.rule.desertShrubs.desc": "Les pousses se transforment en buissons morts dans les climats chauds et sans accès à l'eau", + + // explosionNoBlockDamage + "carpet.rule.explosionNoBlockDamage.desc": "Les explosions ne détruisent pas les blocs", + + // fastRedstoneDust + "carpet.rule.fastRedstoneDust.desc": "Optimisations de performances pour la poussière de redstone", + + "carpet.rule.fastRedstoneDust.extra.0": "par Theosib", + "carpet.rule.fastRedstoneDust.extra.1": ".. corrige également certains comportements de localisation de la redstone vanille MC-11193", + "carpet.rule.fastRedstoneDust.extra.2": "donc le comportement des dispositifs de localisation vanille n'est pas garanti", + + // fillLimit + "carpet.rule.fillLimit.desc": "[Obsolète] Limite personnalisable du volume de remplissage/clone/biomereplace", + + "carpet.rule.fillLimit.extra.0": "Utilisez la règle de jeu vanilla à la place. Ce paramètre sera supprimé dans la version 1.20.0", + + // fillUpdates + "carpet.rule.fillUpdates.desc": "Les commandes fill/clone/setblock et les blocs de structure provoquent des mises à jour de blocs", + + // flippinCactus + "carpet.rule.flippinCactus.desc": "Les joueurs peuvent retourner et faire pivoter les blocs lorsqu'ils tiennent un cactus", + + "carpet.rule.flippinCactus.extra.0": "Ne provoque pas de mises à jour de blocs lorsqu'il est retourné/faire pivoter", + "carpet.rule.flippinCactus.extra.1": "S'applique aux pistons, observateurs, distributeurs, répéteurs, escaliers, terracotta émaillée, etc.", + + // fogOff + "carpet.rule.fogOff.desc": "Supprime le brouillard du client dans le Nether et l'End", + + "carpet.rule.fogOff.extra.0": "Améliore la visibilité, mais l'apparence est étrange", + + // forceloadLimit + "carpet.rule.forceloadLimit.desc": "Limite personnalisable des chunks de chargement forcé", + + // hardcodeTNTangle + "carpet.rule.hardcodeTNTangle.desc": "Définit l'angle aléatoire horizontal de la TNT pour le débogage des dispositifs TNT", + + "carpet.rule.hardcodeTNTangle.extra.0": "Définir sur -1 pour un comportement par défaut", + + // hopperCounters + "carpet.rule.hopperCounters.desc": "Les entonnoirs pointant vers de la laine comptent les objets qui passent à travers eux", + + "carpet.rule.hopperCounters.extra.0": "Active la commande /counter et les actions lors de la pose de tapis rouge et vert sur des blocs de laine", + "carpet.rule.hopperCounters.extra.1": "Utilisez /counter reset pour réinitialiser le compteur et /counter pour interroger", + "carpet.rule.hopperCounters.extra.2": "En mode survie, placez un tapis vert sur de la laine de même couleur pour interroger, rouge pour réinitialiser les compteurs", + "carpet.rule.hopperCounters.extra.3": "Les compteurs sont globaux et partagés entre les joueurs, 16 canaux disponibles", + "carpet.rule.hopperCounters.extra.4": "Les objets comptés sont détruits, comptez jusqu'à une pile par tick par entonnoir", + + // huskSpawningInTemples + "carpet.rule.huskSpawningInTemples.desc": "Seuls les Husks apparaissent dans les temples du désert", + + // interactionUpdates + "carpet.rule.interactionUpdates.desc": "La pose de blocs provoque des mises à jour de blocs", + + // lagFreeSpawning + "carpet.rule.lagFreeSpawning.desc": "L'apparition nécessite beaucoup moins de CPU et de mémoire", + + // language + "carpet.rule.language.desc": "Définit la langue pour Carpet", + + // lightningKillsDropsFix + "carpet.rule.lightningKillsDropsFix.desc": "La foudre détruit les objets qui tombent lorsque la foudre tue une entité", + + "carpet.rule.lightningKillsDropsFix.extra.0": "En le réglant sur true, la foudre ne détruira pas les objets", + "carpet.rule.lightningKillsDropsFix.extra.1": "Corrige MC-206922.", + + // liquidDamageDisabled + "carpet.rule.liquidDamageDisabled.desc": "Désactive la destruction de blocs causée par les liquides en mouvement", + + // maxEntityCollisions + "carpet.rule.maxEntityCollisions.desc": "Limites personnalisables de collisions d'entités maximales, 0 pour aucune limite", + + // mergeTNT + "carpet.rule.mergeTNT.desc": "Fusionne les entités TNT amorcées immobiles", + + // missingTools + "carpet.rule.missingTools.desc": "Le verre peut être cassé plus rapidement avec des pioches", + + // moreBlueSkulls + "carpet.rule.moreBlueSkulls.desc": "Augmente à des fins de test le nombre de têtes bleues tirées par l'Wither", + + // movableAmethyst + "carpet.rule.movableAmethyst.desc": "Permet aux blocs d'améthystes en bourgeonnement d'être déplacés", + + "carpet.rule.movableAmethyst.extra.0": "Permet de les déplacer avec des pistons", + "carpet.rule.movableAmethyst.extra.1": "et ajoute un drop supplémentaire lors de l'extraction avec une pioche enchantée avec Soie Toucher", + + // movableBlockEntities + "carpet.rule.movableBlockEntities.desc": "Les pistons peuvent pousser des blocs entités, comme des entonnoirs, des coffres, etc.", + + // optimizedTNT + "carpet.rule.optimizedTNT.desc": "Le TNT provoque moins de lag lorsqu'il explose au même endroit et dans des liquides", + + // perfPermissionLevel + "carpet.rule.perfPermissionLevel.desc": "Niveau de permission requis pour la commande /perf", + + // persistentParrots + "carpet.rule.persistentParrots.desc": "Les perroquets ne quittent pas vos épaules tant que vous ne subissez pas de dégâts appropriés", + + // piglinsSpawningInBastions + "carpet.rule.piglinsSpawningInBastions.desc": "Les Piglins réapparaissent dans les vestiges des bastions", + + "carpet.rule.piglinsSpawningInBastions.extra.0": "Comprend les piglins, les brutes et quelques hoglins", + + // pingPlayerListLimit + "carpet.rule.pingPlayerListLimit.desc": "Limite d'échantillonnage de la liste des joueurs pour la liste de serveurs ping (menu multijoueur)", + + // placementRotationFix + "carpet.rule.placementRotationFix.desc": "Corrige le problème de rotation de placement de bloc lorsqu'un joueur fait rapidement tourner les blocs en les plaçant", + + // portalCreativeDelay + "carpet.rule.portalCreativeDelay.desc": "Nombre de ticks de délai pour utiliser un portail du Nether en mode Créatif", + + // portalSurvivalDelay + "carpet.rule.portalSurvivalDelay.desc": "Nombre de ticks de délai pour utiliser un portail du Nether en mode Survie", + + // pushLimit + "carpet.rule.pushLimit.desc": "Limite de poussée personnalisable des pistons", + + // quasiConnectivity + "carpet.rule.quasiConnectivity.desc": "Les pistons, les droppers et les distributeurs vérifient la puissance du ou des blocs situés au-dessus d'eux.", + + "carpet.rule.quasiConnectivity.extra.0": "Définit la portée à laquelle les pistons, les droppers et les distributeurs vérifient la 'quasi puissance'.", + + // railPowerLimit + "carpet.rule.railPowerLimit.desc": "Portée personnalisable de puissance des rails alimentés", + + // renewableBlackstone + "carpet.rule.renewableBlackstone.desc": "Générateur de basalte du Nether sans sable des âmes en dessous", + + "carpet.rule.renewableBlackstone.extra.0": ".. se transformera en blackstone à la place", + + // renewableCoral + "carpet.rule.renewableCoral.desc": "Les structures de corail poussent avec de l'engrais à partir de plantes de corail", + + "carpet.rule.renewableCoral.extra.0": "L'extension permet également de faire pousser à partir de ventilateurs de corail pour une culture durable en dehors des océans chauds", + + // renewableDeepslate + "carpet.rule.renewableDeepslate.desc": "La lave et l'eau génèrent de la pierre profonde et de la pierre profonde ciselée en dessous de Y0", + + // renewableSponges + "carpet.rule.renewableSponges.desc": "Les gardiens se transforment en gardiens anciens lorsqu'ils sont frappés par la foudre", + + // rotatorBlock + "carpet.rule.rotatorBlock.desc": "Les cactus dans les distributeurs font tourner les blocs.", + + "carpet.rule.rotatorBlock.extra.0": "Tourne les blocs dans le sens antihoraire si possible", + + // scriptsAppStore + "carpet.rule.scriptsAppStore.desc": "Emplacement du dépôt en ligne des applications scarpet", + + "carpet.rule.scriptsAppStore.extra.0": "Définissez sur 'none' pour désactiver.", + "carpet.rule.scriptsAppStore.extra.1": "Indiquez n'importe quel référentiel github avec des applications scarpet", + "carpet.rule.scriptsAppStore.extra.2": "en utilisant //contents/", + + // scriptsAutoload + "carpet.rule.scriptsAutoload.desc": "Les scripts scarpet des fichiers du monde se chargeront automatiquement au démarrage du serveur/monde", + + "carpet.rule.scriptsAutoload.extra.0": "si /script est activé", + + // scriptsDebugging + "carpet.rule.scriptsDebugging.desc": "Active les messages de débogage des scripts dans le journal système", + + // scriptsOptimization + "carpet.rule.scriptsOptimization.desc": "Active l'optimisation des scripts", + + // sculkSensorRange + "carpet.rule.sculkSensorRange.desc": "Portée personnalisable du capteur sculk", + + // shulkerSpawningInEndCities + "carpet.rule.shulkerSpawningInEndCities.desc": "Les Shulkers réapparaissent dans les cités de l'End", + + // silverFishDropGravel + "carpet.rule.silverFishDropGravel.desc": "Les poissons d'argent laissent tomber un objet gravier lorsqu'ils sortent d'un bloc", + + // simulationDistance + "carpet.rule.simulationDistance.desc": "Modifie la distance de simulation du serveur.", + + "carpet.rule.simulationDistance.extra.0": "Définir sur 0 pour ne pas remplacer la valeur dans les paramètres du serveur.", + + // smoothClientAnimations + "carpet.rule.smoothClientAnimations.desc": "Animation fluide du client avec des paramètres faibles de TPS", + + "carpet.rule.smoothClientAnimations.extra.0": "Fonctionne uniquement en mode un joueur et ralentit les joueurs", + + // spawnChunksSize + "carpet.rule.spawnChunksSize.desc": "Modifie la taille des tronçons de génération", + + "carpet.rule.spawnChunksSize.extra.0": "Définit le nouveau rayon", + "carpet.rule.spawnChunksSize.extra.1": "Définir sur 0 - désactive les tronçons de génération", + + // stackableShulkerBoxes + "carpet.rule.stackableShulkerBoxes.desc": "Les boîtes de shulker vides peuvent s'empiler lorsqu'elles sont jetées au sol.", + + "carpet.rule.stackableShulkerBoxes.extra.0": ".. ou lorsqu'elles sont manipulées à l'intérieur des inventaires", + + // structureBlockIgnored + "carpet.rule.structureBlockIgnored.desc": "Modifie le bloc ignoré par le bloc de structure", + + // structureBlockLimit + "carpet.rule.structureBlockLimit.desc": "Limite personnalisable du bloc de structure sur chaque axe", + + "carpet.rule.structureBlockLimit.extra.0": "AVERTISSEMENT : doit être permanent pour un chargement correct.", + "carpet.rule.structureBlockLimit.extra.1": "Il est recommandé de définir 'structureBlockIgnored' sur 'air'", + "carpet.rule.structureBlockLimit.extra.2": "lors de l'enregistrement de structures massives.", + "carpet.rule.structureBlockLimit.extra.3": "Nécessaire sur le client du joueur qui édite le bloc de structure.", + "carpet.rule.structureBlockLimit.extra.4": "'structureBlockOutlineDistance' peut être nécessaire pour", + "carpet.rule.structureBlockLimit.extra.5": "un rendu correct des structures longues.", + + // structureBlockOutlineDistance + "carpet.rule.structureBlockOutlineDistance.desc": "Distance de rendu personnalisable de l'aperçu du bloc de structure", + + "carpet.rule.structureBlockOutlineDistance.extra.0": "Nécessaire sur le client pour fonctionner correctement", + + // summonNaturalLightning + "carpet.rule.summonNaturalLightning.desc": "L'invocation d'un éclair a tous les effets secondaires d'un éclair naturel", + + // superSecretSetting + "carpet.rule.superSecretSetting.desc": "Gbhs sgnf sadsgras fhskdpri !!!", + + // thickFungusGrowth + "carpet.rule.thickFungusGrowth.desc": "Permet de faire pousser des champignons du Nether avec une base de 3x3 avec de l'engrais", + + "carpet.rule.thickFungusGrowth.extra.0": "Le réglage sur 'all' fera pousser tous les champignons du Nether en arbres de 3x3", + "carpet.rule.thickFungusGrowth.extra.1": "Le réglage sur 'random' fera pousser 6% de tous les champignons du Nether en arbres de 3x3", + "carpet.rule.thickFungusGrowth.extra.2": "(ceci est cohérent avec la génération du monde)", + + // tickSyncedWorldBorders + "carpet.rule.tickSyncedWorldBorders.desc": "Déplace les limites du monde en fonction du temps de jeu plutôt que du temps réel", + + "carpet.rule.tickSyncedWorldBorders.extra.0": "Cela a pour effet que lorsque le taux de ticks change, la vitesse des limites du monde change également proportionnellement", + + // tntDoNotUpdate + "carpet.rule.tntDoNotUpdate.desc": "La TNT ne se met pas à jour lorsqu'elle est placée contre une source de courant", + + // tntPrimerMomentumRemoved + "carpet.rule.tntPrimerMomentumRemoved.desc": "Supprime l'élan aléatoire de la TNT lorsqu'elle est amorcée", + + // tntRandomRange + "carpet.rule.tntRandomRange.desc": "Définit la plage d'explosion aléatoire de la TNT sur une valeur fixe", + + "carpet.rule.tntRandomRange.extra.0": "Définir sur -1 pour le comportement par défaut", + + // updateSuppressionBlock + "carpet.rule.updateSuppressionBlock.desc": "Placer un rail d'activation sur le dessus d'un bloc de barrière remplira la pile de mise à jour des voisins lorsque le rail s'éteint.", + + "carpet.rule.updateSuppressionBlock.extra.0": "L'entier saisi correspond au nombre de mises à jour qui doivent rester dans la pile", + "carpet.rule.updateSuppressionBlock.extra.1": "-1 pour le désactiver", + + // viewDistance + "carpet.rule.viewDistance.desc": "Modifie la distance de vue du serveur.", + + "carpet.rule.viewDistance.extra.0": "Définir sur 0 pour ne pas remplacer la valeur dans les paramètres du serveur.", + + // xpFromExplosions + "carpet.rule.xpFromExplosions.desc": "L'expérience tombera de tous les blocs qui produisent de l'expérience lors d'une explosion de tout type", + + // xpNoCooldown + "carpet.rule.xpNoCooldown.desc": "Les joueurs absorbent instantanément l'expérience, sans délai" + +} diff --git a/src/main/resources/assets/carpet/lang/pt_br.json b/src/main/resources/assets/carpet/lang/pt_br.json new file mode 100644 index 0000000..f10018d --- /dev/null +++ b/src/main/resources/assets/carpet/lang/pt_br.json @@ -0,0 +1,418 @@ +{ + // Settings command UI elements + + "carpet.settings.command.browse_categories": "Procurar categorias", + "carpet.settings.command.version": "versão", + "carpet.settings.command.list_all_category": "lista todas as configurações de %s", + "carpet.settings.command.current_settings_header": "Configurações atuais de %s", + "carpet.settings.command.switch_to": "Alternar para %s", + "carpet.settings.command.unknown_rule": "Regra desconhecida", + "carpet.settings.command.current_from_file_header": "Configurações de inicialização atuais de %s de %s", + "carpet.settings.command.mod_settings_matching": "%s configurações correspondentes \"%s\" ", + "carpet.settings.command.all_mod_settings": "Todas as configurações de %s", + "carpet.settings.command.tags": "Tags", + "carpet.settings.command.change_permanently": "Alterar permanentemente", + "carpet.settings.command.change_permanently_tooltip": "clique para manter as configurações em %s para salvar nas reinicializações", + "carpet.settings.command.default_set": "A regra %s agora será padronizada para %s", + "carpet.settings.command.default_removed": "A regra %s não será mais definida na reinicialização", + "carpet.settings.command.current_value": "valor atual", + + // Categories + + "carpet.category.bugfix": "bugfix", + "carpet.category.survival": "sobrevivência", + "carpet.category.creative": "criativo", + "carpet.category.experimental": "experimental", + "carpet.category.optimization": "otimização", + "carpet.category.feature": "característica", + "carpet.category.command": "comando", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "distribuidor", + "carpet.category.scarpet": "scarpet", + "carpet.category.client": "cliente", + + // Rules + + // allowSpawningOfflinePlayers + "carpet.rule.allowSpawningOfflinePlayers.desc": "Spawna jogadores offline no modo online se o jogador do modo online com o nome especificado não existir", + + // antiCheatDisabled + "carpet.rule.antiCheatDisabled.desc": "Evita que os jogadores façam elásticos ao se moverem muito rápido", + + "carpet.rule.antiCheatDisabled.extra.0": "... ou ser expulso por 'voar'", + "carpet.rule.antiCheatDisabled.extra.1": "Coloca mais confiança no posicionamento dos clientes", + "carpet.rule.antiCheatDisabled.extra.2": "Aumenta a distância de mineração permitida pelo jogador para 32 blocos", + + // carpetCommandPermissionLevel + "carpet.rule.carpetCommandPermissionLevel.desc": "Nível de permissão de comando do carpet. Só pode ser definido via arquivo .conf", + + // carpets + "carpet.rule.carpets.desc": "Colocar Carpet pode emitir comandos de Carpet para jogadores não operacionais", + + // chainStone + "carpet.rule.chainStone.desc": "As correntes vão grudar umas nas outras nas pontas longas", + + "carpet.rule.chainStone.extra.0": "e vai ficar com outros blocos que se conectam a eles diretamente.", + "carpet.rule.chainStone.extra.1": "Com stick_to_all: ele vai ficar mesmo se não estiver conectado visualmente", + + // cleanLogs + "carpet.rule.cleanLogs.desc": "Remove mensagens desagradáveis dos logs", + + "carpet.rule.cleanLogs.extra.0": "Não exibe 'Tamanho máximo do pool de som 247 alcançado'", + "carpet.rule.cleanLogs.extra.1": "O que é normal com farms e engenhocas decentes", + + // commandDistance + "carpet.rule.commandDistance.desc": "Habilita o comando /distance para medir a distância do jogo entre os pontos", + + "carpet.rule.commandDistance.extra.0": "Também ativa a ação de colocação de carpete marrom se a regra de 'Carpet' também estiver ativada", + + // commandDraw + "carpet.rule.commandDraw.desc": "Habilita os comandos /draw", + + "carpet.rule.commandDraw.extra.0": "... permite desenhar formas simples ou", + "carpet.rule.commandDraw.extra.1": "outras formas que são meio difíceis de fazer normalmente", + + // commandInfo + "carpet.rule.commandInfo.desc": "Habilita o comando /info para blocos", + + "carpet.rule.commandInfo.extra.0": "Também permite a ação de colocação de carpete cinza", + "carpet.rule.commandInfo.extra.1": "se a regra 'Carpet' também estiver ativada", + + // commandLog + "carpet.rule.commandLog.desc": "Habilita o comando /log para monitorar eventos via chat e sobreposições", + + // commandPerimeterInfo + "carpet.rule.commandPerimeterInfo.desc": "Habilita o comando /perimeterinfo", + + "carpet.rule.commandPerimeterInfo.extra.0": "... que verifica a área ao redor do quarteirão em busca de possíveis pontos de spawn", + + // commandPlayer + "carpet.rule.commandPlayer.desc": "Habilita o comando /player para controlar/spawnar jogadores", + + // commandProfile + "carpet.rule.commandProfile.desc": "Habilita o comando /profile para monitorar o desempenho do jogo", + + "carpet.rule.commandProfile.extra.0": "subconjunto de recursos de comando /tick", + + // commandScript + "carpet.rule.commandScript.desc": "Habilita o comando /script", + + "carpet.rule.commandScript.extra.0": "Uma API de script no jogo para a linguagem de programação Scarpet", + + // commandScriptACE + "carpet.rule.commandScriptACE.desc": "Habilita restrições para execução de código arbitrário com scarpet", + + "carpet.rule.commandScriptACE.extra.0": "Usuários que não têm esse nível de permissão", + "carpet.rule.commandScriptACE.extra.1": "não poderá carregar aplicativos ou executar /script.", + "carpet.rule.commandScriptACE.extra.2": "É também o nível de permissão que os aplicativos", + "carpet.rule.commandScriptACE.extra.3": "tem ao executar comandos com run()", + + // commandSpawn + "carpet.rule.commandSpawn.desc": "Habilita o comando /spawn para rastreamento de spawn", + + // commandTick + "carpet.rule.commandTick.desc": "Habilita o comando /tick para controlar os relógios do jogo", + + // commandTrackAI + "carpet.rule.commandTrackAI.desc": "Permite rastrear mobs AI via comando /track", + + // creativeFlyDrag + "carpet.rule.creativeFlyDrag.desc": "Arrasto de ar criativo", + + "carpet.rule.creativeFlyDrag.extra.0": "O aumento do arrasto diminuirá a velocidade do seu voo", + "carpet.rule.creativeFlyDrag.extra.1": "Então, precisa ajustar a velocidade de acordo", + "carpet.rule.creativeFlyDrag.extra.2": "Com arrasto 1.0, usar a velocidade de 11 parece combinar com as velocidades do vanilla.", + "carpet.rule.creativeFlyDrag.extra.3": "Configuração puramente do lado do cliente, o que significa que", + "carpet.rule.creativeFlyDrag.extra.4": "defini-lo no servidor dedicado não tem efeito", + "carpet.rule.creativeFlyDrag.extra.5": "mas isso também significa que funcionará em servidores vanilla também", + + // creativeFlySpeed + "carpet.rule.creativeFlySpeed.desc": "Multiplicador de velocidade de vôo criativo", + + "carpet.rule.creativeFlySpeed.extra.0": "Configuração puramente do lado do cliente, o que significa que", + "carpet.rule.creativeFlySpeed.extra.1": "defini-lo no servidor dedicado não tem efeito", + "carpet.rule.creativeFlySpeed.extra.2": "mas isso também significa que funcionará em servidores vanilla também", + + // creativeNoClip + "carpet.rule.creativeNoClip.desc": "Criativo No Clip", + + "carpet.rule.creativeNoClip.extra.0": "Nos servidores, ele precisa ser definido em ambos ", + "carpet.rule.creativeNoClip.extra.1": "cliente e servidor para funcionar corretamente.", + "carpet.rule.creativeNoClip.extra.2": "Não tem efeito quando definido apenas no servidor", + "carpet.rule.creativeNoClip.extra.3": "Pode permitir a fase através das paredes", + "carpet.rule.creativeNoClip.extra.4": "se for apenas no lado do cliente do Carpet", + "carpet.rule.creativeNoClip.extra.5": "mas requer alguma magia de alçapão para", + "carpet.rule.creativeNoClip.extra.6": "permitir que o jogador insira blocos", + + // creativePlayersLoadChunks + "carpet.rule.creativePlayersLoadChunks.desc": "Jogadores criativos carregam pedaços, ou não! Assim como os espectadores!", + + "carpet.rule.creativePlayersLoadChunks.extra.0": "A alternância se comporta exatamente como se o jogador estivesse no modo espectador e a alternância gamerule spectatorsGenerateChunks.", + + // ctrlQCraftingFix + "carpet.rule.ctrlQCraftingFix.desc": "Soltar pilhas inteiras também funciona no slot de resultado da interface do usuário de criação", + + // customMOTD + "carpet.rule.customMOTD.desc": "Define uma mensagem motd diferente no cliente tentando se conectar ao servidor", + + "carpet.rule.customMOTD.extra.0": "use '_' para usar a configuração de inicialização de server.properties", + + // defaultLoggers + "carpet.rule.defaultLoggers.desc": "define esses registradores em suas configurações padrão para todos os novos jogadores", + + "carpet.rule.defaultLoggers.extra.0": "use csv, como 'tps, mobcaps' para vários loggers, nenhum por nada", + + // desertShrubs + "carpet.rule.desertShrubs.desc": "Mudas se transformam em arbustos mortos em climas quentes e sem acesso à água", + + // explosionNoBlockDamage + "carpet.rule.explosionNoBlockDamage.desc": "Explosões não destruirão blocos", + + // fastRedstoneDust + "carpet.rule.fastRedstoneDust.desc": "Otimizações de atraso para poeira de redstone", + + "carpet.rule.fastRedstoneDust.extra.0": "por Theosib", + "carpet.rule.fastRedstoneDust.extra.1": ".. também corrige alguns comportamentos de localização ou redstone vanilla MC-11193", + "carpet.rule.fastRedstoneDust.extra.2": "então o comportamento de engenhocas do vanilla locacionais não é garantido", + + // fillLimit + "carpet.rule.fillLimit.desc": "Limite de volume de fill/clone personalizável", + + // fillUpdates + "carpet.rule.fillUpdates.desc": "fill/clone/setblock e blocos de estrutura causam atualizações de bloco", + + // flatWorldStructureSpawning + "carpet.rule.flatWorldStructureSpawning.desc": "Permite que mobs de estrutura apareçam em mundos planos", + + // flippinCactus + "carpet.rule.flippinCactus.desc": "Os jogadores podem virar e girar blocos ao segurar cactos", + + "carpet.rule.flippinCactus.extra.0": "Não causa atualizações de blocos quando girado/invertido", + "carpet.rule.flippinCactus.extra.1": "Aplica-se a pistões, observadores, conta-gotas, repetidores, escadas, terracota envidraçada etc...", + + // fogOff + "carpet.rule.fogOff.desc": "Remove a névoa do cliente no nether e no final", + + "carpet.rule.fogOff.extra.0": "Melhora a visibilidade, mas parece estranho", + + // forceloadLimit + "carpet.rule.forceloadLimit.desc": "Limite de bloco de carregamento de pedaços personalizável", + + // hardcodeTNTangle + "carpet.rule.hardcodeTNTangle.desc": "Define o ângulo aleatório horizontal em TNT para depuração de engenhocas TNT", + + "carpet.rule.hardcodeTNTangle.extra.0": "Defina como -1 para comportamento padrão", + + // hopperCounters + "carpet.rule.hopperCounters.desc": "funis apontando para a lã contarão os itens que passam por eles", + + "carpet.rule.hopperCounters.extra.0": "Habilita o comando /counter e ações ao colocar Carpet vermelhos e verdes em blocos de lã", + "carpet.rule.hopperCounters.extra.1": "Usar /counter reset para resetar o contador, e /counter para consulta", + "carpet.rule.hopperCounters.extra.2": "Na sobrevivência, coloque Carpet verde na lã da mesma cor para consultar, vermelho para zerar os contadores", + "carpet.rule.hopperCounters.extra.3": "Os contadores são globais e compartilhados entre os jogadores, 16 canais disponíveis", + "carpet.rule.hopperCounters.extra.4": "Os itens contados são destruídos, conte até uma pilha por tick por funil", + + // huskSpawningInTemples + "carpet.rule.huskSpawningInTemples.desc": "Apenas cascas aparecem em templos no deserto", + + // interactionUpdates + "carpet.rule.interactionUpdates.desc": "colocar blocos causa atualizações de blocos", + + // lagFreeSpawning + "carpet.rule.lagFreeSpawning.desc": "A desova requer muito menos CPU e memória", + + // language + "carpet.rule.language.desc": "Define o idioma do Carpet", + + // leadFix + "carpet.rule.leadFix.desc": "Corrige cordas quebrando/tornando-se invisíveis em pedaços descarregados", + + "carpet.rule.leadFix.extra.0": "Você ainda pode obter links de trela visivelmente quebrados no lado do cliente, mas no lado do servidor o link ainda está lá.", + + // lightningKillsDropsFix + "carpet.rule.lightningKillsDropsFix.desc": "O relâmpago mata os itens que caem quando o relâmpago mata uma entidade", + + "carpet.rule.lightningKillsDropsFix.extra.0": "Definir como true impedirá que os raios matem gotas", + "carpet.rule.lightningKillsDropsFix.extra.1": "Correções [MC-206922](https://bugs.mojang.com/browse/MC-206922).", + + // liquidDamageDisabled + "carpet.rule.liquidDamageDisabled.desc": "Desabilita a quebra de blocos causada pelo fluxo de líquidos", + + // maxEntityCollisions + "carpet.rule.maxEntityCollisions.desc": "Limites máximos de colisão de entidades personalizáveis, 0 para sem limites", + + // mergeTNT + "carpet.rule.mergeTNT.desc": "Mescla entidades TNT preparadas estacionárias", + + // missingTools + "carpet.rule.missingTools.desc": "Vidro pode ser quebrado mais rápido com picaretas", + + // moreBlueSkulls + "carpet.rule.moreBlueSkulls.desc": "Aumenta para fins de teste o número de caveiras azuis disparadas pela cernelha", + + // movableAmethyst + "carpet.rule.movableAmethyst.desc": "Permite que blocos de Ametista Brotando sejam movidos", + + "carpet.rule.movableAmethyst.extra.0": "Permitir que eles sejam movidos por pistões", + "carpet.rule.movableAmethyst.extra.1": "bem como adiciona queda extra ao minerar com picareta de toque de seda", + + // movableBlockEntities + "carpet.rule.movableBlockEntities.desc": "Os pistões podem empurrar entidades de bloco, como funis, baús, etc.", + + // optimizedTNT + "carpet.rule.optimizedTNT.desc": "TNT causa menos atraso ao explodir no mesmo local e em líquidos", + + // perfPermissionLevel + "carpet.rule.perfPermissionLevel.desc": "Nível de permissão necessário para o comando /perf", + + // persistentParrots + "carpet.rule.persistentParrots.desc": "Papagaios não saem de seus ombros até que você receba o dano adequado", + + // piglinsSpawningInBastions + "carpet.rule.piglinsSpawningInBastions.desc": "Piglins vão reaparecer em remanescentes de bastiões", + + "carpet.rule.piglinsSpawningInBastions.extra.0": "Inclui piglins, brutos e alguns hoglins", + + // pingPlayerListLimit + "carpet.rule.pingPlayerListLimit.desc": "Ping da lista de servidores personalizável (menu Multiplayer) limite de amostra da lista de jogadores", + + // placementRotationFix + "carpet.rule.placementRotationFix.desc": "corrige o problema de rotação de posicionamento de blocos quando o jogador gira rapidamente ao colocar blocos", + + // portalCreativeDelay + "carpet.rule.portalCreativeDelay.desc": "Quantidade de ticks de atraso para usar um portal do Nether no criativo", + + // portalSurvivalDelay + "carpet.rule.portalSurvivalDelay.desc": "Quantidade de tiques de atraso para usar um portal do Nether na sobrevivência", + + // pushLimit + "carpet.rule.pushLimit.desc": "Limite de pressão do pistão personalizável", + + // quasiConnectivity + "carpet.rule.quasiConnectivity.desc": "Pistões, conta-gotas e dispensadores reagem se o bloco acima deles for alimentado", + + // railPowerLimit + "carpet.rule.railPowerLimit.desc": "Faixa de potência de trilho alimentado personalizável", + + // renewableBlackstone + "carpet.rule.renewableBlackstone.desc": "Gerador de basalto do Nether sem areia da alma abaixo ", + + "carpet.rule.renewableBlackstone.extra.0": " .. se converterá em pedra-negra em vez disso", + + // renewableCoral + "carpet.rule.renewableCoral.desc": "Estruturas de coral crescerão com farinha de ossos de plantas de coral", + + "carpet.rule.renewableCoral.extra.0": "Expandido também permite o cultivo de fãs de corais para agricultura sustentável fora dos oceanos quentes", + + // renewableDeepslate + "carpet.rule.renewableDeepslate.desc": "Lava e água geram ardósia profunda e ardósia pavimentada em vez de abaixo Y0", + + // renewableSponges + "carpet.rule.renewableSponges.desc": "Guardiões se transformam em Elder Guardian quando atingidos por um raio", + + // rotatorBlock + "carpet.rule.rotatorBlock.desc": "Cactus em dispensadores gira blocos.", + + "carpet.rule.rotatorBlock.extra.0": "Gira o bloco no sentido anti-horário, se possível", + + // scriptsAppStore + "carpet.rule.scriptsAppStore.desc": "Localização do repositório online de aplicativos scarpet", + + "carpet.rule.scriptsAppStore.extra.0": "defina como 'none' para desabilitar.", + "carpet.rule.scriptsAppStore.extra.1": "Aponte para qualquer repositório do github com aplicativos scarpet", + "carpet.rule.scriptsAppStore.extra.2": "usando //content/", + + // scriptsAutoload + "carpet.rule.scriptsAutoload.desc": "O script Scarpet dos arquivos do mundo será carregado automaticamente no início do servidor/mundo ", + + "carpet.rule.scriptsAutoload.extra.0": "se /script estiver habilitado", + + // scriptsDebugging + "carpet.rule.scriptsDebugging.desc": "Habilita mensagens de depuração de scripts no log do sistema", + + // scriptsOptimization + "carpet.rule.scriptsOptimization.desc": "Habilita a otimização de scripts", + + // sculkSensorRange + "carpet.rule.sculkSensorRange.desc": "Gama de sensores sculk personalizável", + + // shulkerSpawningInEndCities + "carpet.rule.shulkerSpawningInEndCities.desc": "Shulkers vão reaparecer nas cidades finais", + + // silverFishDropGravel + "carpet.rule.silverFishDropGravel.desc": "Silverfish dropa um item de cascalho ao sair de um bloco", + + // simulationDistance + "carpet.rule.simulationDistance.desc": "Altera a distância de simulação do servidor.", + + "carpet.rule.simulationDistance.extra.0": "Defina como 0 para não substituir o valor nas configurações do servidor.", + + // smoothClientAnimations + "carpet.rule.smoothClientAnimations.desc": "animações de cliente suaves com configurações de tps baixas", + + "carpet.rule.smoothClientAnimations.extra.0": "funciona apenas em SP, e vai deixar os jogadores mais lentos", + + // spawnChunksSize + "carpet.rule.spawnChunksSize.desc": "Muda o tamanho dos pedaços de spawn", + + "carpet.rule.spawnChunksSize.extra.0": "Define novo raio", + "carpet.rule.spawnChunksSize.extra.1": "definindo-o como 0 - desativa pedaços de desova", + + // stackableShulkerBoxes + "carpet.rule.stackableShulkerBoxes.desc": "Caixas de shulker vazias podem empilhar quando jogadas no chão.", + + "carpet.rule.stackableShulkerBoxes.extra.0": ".. ou quando manipulados dentro dos estoques", + + // structureBlockIgnored + "carpet.rule.structureBlockIgnored.desc": "Altera o bloco ignorado pelo Bloco de Estrutura", + + // structureBlockLimit + "carpet.rule.structureBlockLimit.desc": "Limite de bloco de estrutura personalizável de cada eixo", + + "carpet.rule.structureBlockLimit.extra.0": "AVISO: Precisa ser permanente para carregamento correto.", + "carpet.rule.structureBlockLimit.extra.1": "Definir 'structureBlockIgnored' para ar é recomendado", + "carpet.rule.structureBlockLimit.extra.2": "ao salvar estruturas maciças.", + "carpet.rule.structureBlockLimit.extra.3": "Obrigatório no cliente do jogador que está editando o Bloco de Estrutura.", + "carpet.rule.structureBlockLimit.extra.4": "'structureBlockOutlineDistance' pode ser necessário para", + "carpet.rule.structureBlockLimit.extra.5": "renderização correta de estruturas longas.", + + // structureBlockOutlineDistance + "carpet.rule.structureBlockOutlineDistance.desc": "Distância de renderização de contorno de bloco de estrutura personalizável", + + "carpet.rule.structureBlockOutlineDistance.extra.0": "Obrigatório no cliente para funcionar corretamente", + + // summonNaturalLightning + "carpet.rule.summonNaturalLightning.desc": "invocar um relâmpago tem todos os efeitos colaterais do relâmpago natural", + + // superSecretSetting + "carpet.rule.superSecretSetting.desc": "Gbhs sgnf sadsgras fhskdpri!!!", + + // tntDoNotUpdate + "carpet.rule.tntDoNotUpdate.desc": "TNT não atualiza quando colocado contra uma fonte de energia", + + // tntPrimerMomentumRemoved + "carpet.rule.tntPrimerMomentumRemoved.desc": "Remove o momento aleatório de TNT quando preparado", + + // tntRandomRange + "carpet.rule.tntRandomRange.desc": "Define o intervalo de explosão aleatória tnt para um valor fixo", + + "carpet.rule.tntRandomRange.extra.0": "Defina como -1 para comportamento padrão", + + // updateSuppressionBlock + "carpet.rule.updateSuppressionBlock.desc": "Colocar um trilho ativador em cima de um bloco de barreira preencherá a pilha do atualizador vizinho quando o trilho for desligado.", + + "carpet.rule.updateSuppressionBlock.extra.0": "O inteiro inserido é a quantidade de atualizações que devem ser deixadas na pilha", + "carpet.rule.updateSuppressionBlock.extra.1": "-1 para desliga", + + // viewDistance + "carpet.rule.viewDistance.desc": "Altera a distância de visualização do servidor.", + + "carpet.rule.viewDistance.extra.0": "Defina como 0 para não substituir o valor nas configurações do servidor.", + + // xpFromExplosions + "carpet.rule.xpFromExplosions.desc": "A experiência cairá de todas as experiências, exceto blocos com qualquer tipo de explosão", + + // xpNoCooldown + "carpet.rule.xpNoCooldown.desc": "Os jogadores absorvem XP instantaneamente, sem demora" + +} 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 0000000..fc455ec --- /dev/null +++ b/src/main/resources/assets/carpet/lang/zh_cn.json @@ -0,0 +1,273 @@ +{ + "carpet.rule.antiCheatDisabled.name": "禁用反作弊移动监测", + "carpet.rule.antiCheatDisabled.desc": "防止玩家因为“本服务器未启用飞行”,“移动过快”等原因被踢出", + + "carpet.rule.carpets.name": "多功能地毯", + "carpet.rule.carpets.desc": "通过放置地毯方块快捷执行部分功能", + + "carpet.rule.combineXPOrbs.name": "经验球合并", + "carpet.rule.combineXPOrbs.desc": "会将多个小经验球合并为更大的经验球", + + "carpet.rule.commandDistance.name": "距离测量", + "carpet.rule.commandDistance.desc": "启用/distance命令以测量游戏中两点间距离", + "carpet.rule.commandDistance.extra.0": "可以通过放置棕色地毯的方式使用本功能,前提是“多功能地毯”已启用", + + "carpet.rule.commandDraw.name": "画图", + "carpet.rule.commandDraw.desc": "启用/draw命令以允许绘制一些简单图形", + + "carpet.rule.commandInfo.name": "获取方块数据", + "carpet.rule.commandInfo.desc": "启用/info命令以获取方块数据", + "carpet.rule.commandInfo.extra.0": "可以通过放置灰色地毯的方式使用本功能,前提是“多功能地毯”已启用", + + "carpet.rule.commandLog.name": "游戏数据监视器", + "carpet.rule.commandLog.desc": "启用/log命令以在聊天栏或Tab栏监视游戏部分数据", + + "carpet.rule.commandPerimeterInfo.name": "刷怪区域搜寻器", + "carpet.rule.commandPerimeterInfo.desc": "启用/perimeterinfo命令以搜寻周围的可刷怪区域", + + "carpet.rule.commandPlayer.name": "玩家控制", + "carpet.rule.commandPlayer.desc": "启用/player命令以控制/召唤玩家", + + "carpet.rule.commandScript.name": "Carpet脚本控制器", + "carpet.rule.commandScript.desc": "启用/script相关命令", + "carpet.rule.commandScript.extra.0": "一个Scarpet语言的游戏内API", + + "carpet.rule.commandSpawn.name": "刷怪追踪器", + "carpet.rule.commandSpawn.desc": "启用/spawn命令以追踪怪物生成", + + "carpet.rule.commandTick.name": "游戏时钟控制器", + "carpet.rule.commandTick.desc": "启用/tick命令以控制游戏时钟", + + "carpet.rule.commandTrackAI.name": "AI追踪器", + "carpet.rule.commandTrackAI.desc": "启用/track命令以追踪生物的AI", + + "carpet.rule.ctrlQCraftingFix.name": "Ctrl+Q合成修复", + "carpet.rule.ctrlQCraftingFix.desc": "在合成时允许使用Ctrl+Q快捷键扔出结果格内所有物品", + + "carpet.rule.customMOTD.name": "MOTD更改", + "carpet.rule.customMOTD.desc": "设置一个不同的MOTD信息", + "carpet.rule.customMOTD.extra.0": "使用“_”时将发送默认MOTD(从server.properties中读取)", + + "carpet.rule.defaultLoggers.name": "默认游戏监视器", + "carpet.rule.defaultLoggers.desc": "为所有新进入玩家设置部分监视器默认开启", + "carpet.rule.defaultLoggers.extra.0": "比如设置“tps,mobcaps”作为默认监视器", + + "carpet.rule.desertShrubs.name": "树苗在沙漠干枯", + "carpet.rule.desertShrubs.desc": "树苗将在附近无水源的沙漠群系枯萎", + + "carpet.rule.disableSpawnChunks.name": "允许出生点区块卸载", + "carpet.rule.disableSpawnChunks.desc": "允许出生点区块卸载", + + "carpet.rule.explosionNoBlockDamage.name": "防爆", + "carpet.rule.explosionNoBlockDamage.desc": "任何形式的爆炸都不会摧毁方块", + + "carpet.rule.extremeBehaviours.name": "随机极端情况", + "carpet.rule.extremeBehaviours.desc": "大幅度提升极端情况出现概率,仅用于测试", + "carpet.rule.extremeBehaviours.extra.0": "发射器和投掷器将使用最大随机刻", + + "carpet.rule.fastRedstoneDust.name": "红石粉卡顿优化", + "carpet.rule.fastRedstoneDust.desc": "红石粉卡顿优化 作者:Theosib", + + "carpet.rule.fillLimit.name": "fill/clone上限更改", + "carpet.rule.fillLimit.desc": "将fill/clone上限更改为自己需要的值", + "carpet.rule.fillLimit.extra.0": "你必须在1-20M之间取一个值", + + "carpet.rule.fillUpdates.name": "fill更新开关", + "carpet.rule.fillUpdates.desc": "fill/clone/setblock命令以及结构方块执行时是否产生更新", + "carpet.rule.fillUpdates.extra.0": "设置为true有更新,设置为false时为无更新", + + "carpet.rule.flatWorldStructureSpawning.name": "结构在超平坦世界生成", + "carpet.rule.flatWorldStructureSpawning.desc": "允许结构在超平坦世界生成", + + "carpet.rule.flippinCactus.name": "仙人掌扳手", + "carpet.rule.flippinCactus.desc": "允许使用仙人掌调整方块朝向,并且不会产生更新", + + "carpet.rule.hardcodeTNTangle.name": "硬编码TNT角度", + "carpet.rule.hardcodeTNTangle.desc": "把TNT水平随机角度设为固定值,可用于测试机器", + "carpet.rule.hardcodeTNTangle.extra.0": "默认为-1,必须在0与360之间,或者-1", + + "carpet.rule.hopperCounters.name": "漏斗计数器", + "carpet.rule.hopperCounters.desc": "漏斗指向的羊毛会计算经过的物品", + "carpet.rule.hopperCounters.extra.0": "在羊毛上放置红色或绿色地毯,使用/counter命令可启用", + "carpet.rule.hopperCounters.extra.1": "使用 /counter <颜色> reset重置计数器,使用/counter <颜色>查询", + "carpet.rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放绿地毯可以查询,放红地毯重置", + "carpet.rule.hopperCounters.extra.3": "数据是全局性在玩家间共享的,并且有16颜色可用", + "carpet.rule.hopperCounters.extra.4": "物品会被销毁,每个漏斗每刻销毁一组物品", + + "carpet.rule.huskSpawningInTemples.name": "尸壳在神殿里生成", + "carpet.rule.huskSpawningInTemples.desc": "只有尸壳会在沙漠神殿生成", + + "carpet.rule.lagFreeSpawning.name": "无卡顿生成", + "carpet.rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和内存", + + "carpet.rule.leadFix.name": "栓绳修复", + "carpet.rule.leadFix.desc": "修复栓绳在未加载区块中,变得不可见的问题", + + "carpet.rule.maxEntityCollisions.name": "最大实体挤压数", + "carpet.rule.maxEntityCollisions.desc": "设定最大实体挤压限制,0表示无限大", + + "carpet.rule.mergeTNT.name": "合并TNT", + "carpet.rule.mergeTNT.desc": "合并静止的点燃的TNT实体", + + "carpet.rule.missingTools.name": "工具缺失修复", + "carpet.rule.missingTools.desc": "活塞,玻璃和海绵可以用合适的工具更快地破坏", + + "carpet.rule.movableBlockEntities.name": "容器移动", + "carpet.rule.movableBlockEntities.desc": "活塞可推动方块实体,比如箱子,漏斗", + + "carpet.rule.onePlayerSleeping.name": "单个玩家睡觉", + "carpet.rule.onePlayerSleeping.desc": "服务器里一个玩家睡觉即可跳过夜晚", + + "carpet.rule.optimizedTNT.name": "TNT优化", + "carpet.rule.optimizedTNT.desc": "TNT在相同的地点或在流体里,爆炸造成更小的延迟", + + "carpet.rule.persistentParrots.name": "鹦鹉停留", + "carpet.rule.persistentParrots.desc": "鹦鹉不会离开你的肩膀,除非你受到伤害", + + "carpet.rule.placementRotationFix.name": "放置旋转修复", + "carpet.rule.placementRotationFix.desc": "修复了玩家放置方块时快速转身造成的问题", + + "carpet.rule.pushLimit.name": "自定义推动上限", + "carpet.rule.pushLimit.desc": "自定义活塞推动上限", + "carpet.rule.pushLimit.extra.0": "你必须在1-1024中选取一个值", + + "carpet.rule.quasiConnectivity.name": "半连接激活开关", + "carpet.rule.quasiConnectivity.desc": "活塞,发射器和投掷器在他们上方方块激活时是否响应", + "carpet.rule.quasiConnectivity.extra.0": "设置为true为开启,设置为false为禁用", + + "carpet.rule.railPowerLimit.name": "自定义动力铁轨激活距离", + "carpet.rule.railPowerLimit.desc": "动力铁轨激活上限可自定义", + "carpet.rule.railPowerLimit.extra.0": "你必须在1-1024中选取一个值", + + "carpet.rule.renewableCoral.name": "珊瑚可再生", + "carpet.rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以生成珊瑚结构", + + "carpet.rule.renewableSponges.name": "海绵可再生", + "carpet.rule.renewableSponges.desc": "守卫者将在被雷击后会变为远古守卫者", + + "carpet.rule.rotatorBlock.name": "仙人掌扳手发射器版", + "carpet.rule.rotatorBlock.desc": "发射器中的仙人掌可以旋转方块(尽可能以逆时针)", + + "carpet.rule.scriptsAutoload.name": "Scarpet脚本自动加载", + "carpet.rule.scriptsAutoload.desc": "在服务器/世界加载的时候将会自动从世界文件中加载Scarpet脚本文件", + "carpet.rule.scriptsAutoload.extra.0": "/script命令必须启用", + + "carpet.rule.shulkerSpawningInEndCities.name": "潜影贝重生", + "carpet.rule.shulkerSpawningInEndCities.desc": "潜影贝将会在末地城中重生", + + "carpet.rule.silverFishDropGravel.name": "蠹虫钻出方块掉落沙砾", + "carpet.rule.silverFishDropGravel.desc": "蠹虫钻出方块时会掉落沙砾", + + "carpet.rule.smoothClientAnimations.name": "平滑客户端动画", + "carpet.rule.smoothClientAnimations.desc": "低TPS设置时可以使客户端动画更加平滑", + "carpet.rule.smoothClientAnimations.extra.0": "仅在单人游戏中生效,并且会降低客户端运行速度", + + "carpet.rule.stackableShulkerBoxes.name": "潜影盒堆叠", + "carpet.rule.stackableShulkerBoxes.desc": "空潜影盒仍在地上时将自动堆叠,直至堆叠为1组", + "carpet.rule.stackableShulkerBoxes.extra.0": "在物品GUI中,请使用Shift+单击以移动整组潜影盒", + + "carpet.rule.summonNaturalLightning.name": "召唤闪电自然化", + "carpet.rule.summonNaturalLightning.desc": "召唤的闪电将拥有所有自然生成闪电拥有的特性", + + "carpet.rule.tntDoNotUpdate.name": "TNT放置时不产生更新", + "carpet.rule.tntDoNotUpdate.desc": "TNT在放置时不会产生任何方块更新(不会激活)", + + "carpet.rule.tntPrimerMomentumRemoved.name": "移除TNT点燃时随机动量", + "carpet.rule.tntPrimerMomentumRemoved.desc": "TNT点燃时的随机动量将被移除", + + "carpet.rule.tntRandomRange.name": "TNT爆炸范围设置", + "carpet.rule.tntRandomRange.desc": "设置TNT随机爆炸范围为一个固定的值,设为-1以禁用", + "carpet.rule.tntRandomRange.extra.0": "必须启用TNT优化功能", + "carpet.rule.tntRandomRange.extra.1": "不能为负值,除了-1", + + "carpet.rule.unloadedEntityFix.name": "未加载实体修复", + "carpet.rule.unloadedEntityFix.desc": "进入或者被推入未加载区块的实体不会消失", + + "carpet.rule.viewDistance.name": "自定义视距", + "carpet.rule.viewDistance.desc": "改变服务器视距,设为0以使用默认值", + "carpet.rule.viewDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数", + + "carpet.rule.simulationDistance.name": "自定义模拟视距", + "carpet.rule.simulationDistance.desc": "改变服务器模拟视距,设为0以使用默认值", + "carpet.rule.simulationDistance.extra.0": "你必须在0(使用服务器默认设置)-32取一个整数", + + "carpet.rule.xpNoCooldown.name": "经验吸收无冷却", + "carpet.rule.xpNoCooldown.desc": "玩家吸收经验球时不会有冷却时间", + + "carpet.rule.portalSuffocationFix.name": "传送门窒息修复", + "carpet.rule.portalSuffocationFix.desc": "使下界传送门正确地传送实体", + "carpet.rule.portalSuffocationFix.extra.0": "传送的实体将不再卡在黑曜石里面", + + "carpet.rule.portalCreativeDelay.name": "创造玩家不被地狱门传送", + "carpet.rule.portalCreativeDelay.desc": "创造模式的玩家将不会立即被地狱门传送", + + "carpet.rule.commandProfile.name": "游戏性能监视器", + "carpet.rule.commandProfile.desc": "启用/profile命令以监视游戏性能", + "carpet.rule.commandProfile.extra.0": "/tick命令功能的部分增强版", + + "carpet.rule.commandScriptACE.name": "地毯脚本使用权限", + "carpet.rule.commandScriptACE.desc": "限制无权限的玩家执行地毯脚本", + "carpet.rule.commandScriptACE.extra.0": "无此权限的玩家无法执行/script run指令或者加载脚本", + + "carpet.rule.creativeFlyDrag.name": "创造飞行阻力系数", + "carpet.rule.creativeFlyDrag.desc": "允许使用本功能更改创造模式下飞行时的空气阻力系数", + "carpet.rule.creativeFlyDrag.extra.0": "阻力增加会降低您的飞行速度,\n所以需要相应地调整速度,\n使用1.0阻力系数的时候,使用11的速度系数似乎与原版速度相匹配。\n本功能仅在客户端也安装Carpet Mod(并且版本>=服务器安装版本)的情况下生效", + + "carpet.rule.creativeFlySpeed.name": "创造飞行速度系数", + "carpet.rule.creativeFlySpeed.desc": "允许使用本功能更改创造模式下飞行速度系数", + "carpet.rule.creativeFlySpeed.extra.0": "本功能仅在客户端也安装Carpet Mod(并且版本>=服务器安装版本)的情况下生效", + + "carpet.rule.creativeNoClip.name": "创造玩家无碰撞检测", + "carpet.rule.creativeNoClip.desc": "创造玩家可以穿过方块", + "carpet.rule.creativeNoClip.extra.0": "这个选项需要服务端和客户端同时支持才生效。\n仅在服务端设置是无效的,而仅在客户端设置的情况下\n你可以利用一些和活板门有关的技巧来穿墙", + + "carpet.rule.fogOff.name": "关闭雾气", + "carpet.rule.fogOff.desc": "启用本功能以关闭地狱与末地的雾气", + "carpet.rule.fogOff.extra.0": "提高了可见度,但看起来很奇怪", + + "carpet.rule.forceloadLimit.name": "永久加载区块上限更改", + "carpet.rule.forceloadLimit.desc": "允许使用本功能更改/forceload命令标记的永久加载区块上限", + + "carpet.rule.interactionUpdates.name": "放置方块产生更新", + "carpet.rule.interactionUpdates.desc": "放置方块是否产生更新的开关,设为false以避免产生更新", + + "carpet.rule.language.name": "语言", + "carpet.rule.language.desc": "为地毯Mod设置所需语言", + + "carpet.rule.portalSurvivalDelay.name": "生存模式地狱门传送延迟", + "carpet.rule.portalSurvivalDelay.desc": "设置生存模式下地狱门传送的延迟(单位:tick)", + + "carpet.rule.spawnChunksSize.name": "出生点永久加载区块半径", + "carpet.rule.spawnChunksSize.desc": "更改出生点永久加载区块半径", + "carpet.rule.spawnChunksSize.extra.0": "定义新半径\n设为0以禁用出生点区块加载", + + + "carpet.category.bugfix": "漏洞修复", + "carpet.category.survival": "生存", + "carpet.category.creative": "创造", + "carpet.category.experimental": "试验性", + "carpet.category.optimization": "优化", + "carpet.category.feature": "特性", + "carpet.category.command": "指令", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "发射器", + "carpet.category.scarpet": "Scarpet脚本语言", + "carpet.category.client": "客户端", + + "carpet.settings.command.browse_categories": "浏览分类", + "carpet.settings.command.version": "版本", + "carpet.settings.command.list_all_category": "列出所有 %s 选项", + "carpet.settings.command.current_settings_header": "目前启用的 %s 选项", + "carpet.settings.command.switch_to": "切换至%s", + "carpet.settings.command.unknown_rule": "未知的规则选项", + "carpet.settings.command.current_from_file_header": "目前启用的%s选项,从%s中读取得", + "carpet.settings.command.mod_settings_matching": "%s中符合“%s”的选项:", + "carpet.settings.command.all_mod_settings": "所有的%s选项", + "carpet.settings.command.tags": "分类", + "carpet.settings.command.change_permanently": "永久更改", + "carpet.settings.command.change_permanently_tooltip": "点击此按钮以使此选项保存于%s,重启后依旧生效", + "carpet.settings.command.default_set": "规则“%s”将会被默认设置为%s", + "carpet.settings.command.default_removed": "规则“%s”将在重启后恢复默认值", + "carpet.settings.command.current_value": "当前值" + +} 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 0000000..9f68e0e --- /dev/null +++ b/src/main/resources/assets/carpet/lang/zh_tw.json @@ -0,0 +1,316 @@ +{ + "carpet.rule.antiCheatDisabled.name": "關閉伺服器作弊檢測", + "carpet.rule.antiCheatDisabled.desc": "防止玩家因為“本伺服器不允許飛行”,“移動速度過快”等原因被請出伺服器", + + "carpet.rule.carpets.name": "放置地毯顯示進階數據", + "carpet.rule.carpets.desc": "通過把地毯放置在地上取得更進階信息", + + "carpet.rule.combineXPOrbs.name": "合併小經驗球", + "carpet.rule.combineXPOrbs.desc": "會將大量的小經驗球合併成等效的大經驗球", + + "carpet.rule.commandCameramode.name": "旁觀者模式", + "carpet.rule.commandCameramode.desc": "允許玩家使用/c切換成旁觀者模式,利用/s切換回生存模式", + "carpet.rule.commandCameramode.extra.0": "/c及/s不需要op權限也可使用", + + "carpet.rule.commandDistance.name": "距離測定", + "carpet.rule.commandDistance.desc": "使用/distance指令測定兩點的距離", + "carpet.rule.commandDistance.extra.0": "可以使用放置棕色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用", + + "carpet.rule.commandDraw.name": "繪製圖形", + "carpet.rule.commandDraw.desc": "使用/draw指令以允許繪製一些簡易圖形", + + "carpet.rule.commandInfo.name": "獲取方塊進階資訊", + "carpet.rule.commandInfo.desc": "允許使用/info指令以獲得方塊進階資訊", + "carpet.rule.commandInfo.extra.0": "可以使用放置灰色地毯來使用此功能,前提是“放置地毯顯示進階數據”已啟用", + + "carpet.rule.commandLog.name": "遊戲數據追蹤器", + "carpet.rule.commandLog.desc": "允許使用/log指令使在Tab欄位追蹤遊戲部分數據", + + "carpet.rule.commandPerimeterInfo.name": "刷怪區域追蹤器", + "carpet.rule.commandPerimeterInfo.desc": "允許使用/perimeterinfo指令以探查周圍可生怪區域", + + "carpet.rule.commandPlayer.name": "機器人控制", + "carpet.rule.commandPlayer.desc": "允許使用/player指令以控制/召喚已存在的玩家id作為機器人", + + "carpet.rule.commandScript.name": "地毯腳本控制器", + "carpet.rule.commandScript.desc": "允許使用/script相關指令", + "carpet.rule.commandScript.extra.0": "一個Scarpet程式語言的遊戲內API", + + "carpet.rule.commandSpawn.name": "刷怪追蹤器", + "carpet.rule.commandSpawn.desc": "允許使用/spawn指令用於怪物生成追蹤", + + "carpet.rule.commandTick.name": "遊戲時鐘控制器", + "carpet.rule.commandTick.desc": "允許使用/tick指令控制遊戲內的伺服器端運行速度", + + "carpet.rule.commandTrackAI.name": "AI追蹤器", + "carpet.rule.commandTrackAI.desc": "允許使用/track指令以追蹤生物的AI", + + "carpet.rule.ctrlQCraftingFix.name": "ctrlQ合成修復", + "carpet.rule.ctrlQCraftingFix.desc": "在合成時允許使用ctrl+Q快捷鍵扔出結果格內所有物品", + + "carpet.rule.customMOTD.name": "MOTD更改", + "carpet.rule.customMOTD.desc": "設置一個不同的MOTD信息", + "carpet.rule.customMOTD.extra.0": "設置成'_'時將採用默認的MOTD(從server.properties中讀取的)", + + "carpet.rule.defaultLoggers.name": "默認開啟的遊戲數據追蹤器", + "carpet.rule.defaultLoggers.desc": "為所有新加入的玩家設置部分遊戲數據追蹤器默認為開啟狀態", + "carpet.rule.defaultLoggers.extra.0": "比如設置'tps,mobcaps'最為默認遊戲數據追蹤", + + "carpet.rule.desertShrubs.name": "樹苗在沙漠中乾枯", + "carpet.rule.desertShrubs.desc": "樹苗將在附近無水源狀態的沙漠生態群系變成枯灌木", + + "carpet.rule.disableSpawnChunks.name": "允許出生點區塊被卸載", + "carpet.rule.disableSpawnChunks.desc": "允許出生點區塊被卸載", + + "carpet.rule.explosionNoBlockDamage.name": "爆炸不破壞方塊", + "carpet.rule.explosionNoBlockDamage.desc": "任何形式的爆炸都無法摧毀方塊", + + "carpet.rule.extremeBehaviours.name": "隨機參數極端化", + "carpet.rule.extremeBehaviours.desc": "大幅度提升極端情況出現的概率,僅用於測試", + "carpet.rule.extremeBehaviours.extra.0": "發射器和投擲器將使用最大隨機刻", + + "carpet.rule.fastRedstoneDust.name": "紅石粉卡頓優化", + "carpet.rule.fastRedstoneDust.desc": "紅石粉卡頓優化 作者 Theosib", + + "carpet.rule.fillLimit.name": "fill/clone上限更改", + "carpet.rule.fillLimit.desc": "將fill/clone上限更改為自己設定的值", + "carpet.rule.fillLimit.extra.0": "你必須在1-2千萬之間取一個整數值", + + "carpet.rule.fillUpdates.name": "fill更新設置", + "carpet.rule.fillUpdates.desc": "fill/clone/setblock指令以及結構方塊執行時是否產生更新", + "carpet.rule.fillUpdates.extra.0": "設置為true時有更新 設置為false時為無更新", + + "carpet.rule.flatWorldStructureSpawning.name": "特定建築怪在超平坦世界生成", + "carpet.rule.flatWorldStructureSpawning.desc": "允許特定建築的怪物在超平坦世界生成", + + "carpet.rule.flippinCactus.name": "仙人掌扳手", + "carpet.rule.flippinCactus.desc": "允許使用仙人掌調整方塊朝向,並且不會造成更新", + + "carpet.rule.hardcodeTNTangle.name": "強制TNT角度", + "carpet.rule.hardcodeTNTangle.desc": "把TNT水平隨機角度設定為固定值,可用於測試機器", + "carpet.rule.hardcodeTNTangle.extra.0": "默認為-1,必須在0與360之間,或者-1", + + "carpet.rule.hopperCounters.name": "漏斗計數器", + "carpet.rule.hopperCounters.desc": "漏斗指向的羊毛方塊會計算吸收的物品並清除", + "carpet.rule.hopperCounters.extra.0": "在羊毛方塊上放置紅色或綠色地毯,使用/counter指令可啟用", + "carpet.rule.hopperCounters.extra.1": "使用 /counter reset重置計數器,使用/counter 查詢資訊", + "carpet.rule.hopperCounters.extra.2": "在生存模式,在同色羊毛上放置綠色地毯可以查詢,放紅色地毯重置", + "carpet.rule.hopperCounters.extra.3": "數據是全伺服器玩家共享的,並且有16色可以使用", + "carpet.rule.hopperCounters.extra.4": "物品會被銷毀,每個漏斗每tick銷毀一組物品", + + "carpet.rule.huskSpawningInTemples.name": "屍殼在沙漠神殿裡生成", + "carpet.rule.huskSpawningInTemples.desc": "只有屍殼會在沙漠神殿生成", + + "carpet.rule.lagFreeSpawning.name": "延遲自然生成", + "carpet.rule.lagFreeSpawning.desc": "生物生成需要更少的CPU和RAM", + + "carpet.rule.leadFix.name": "栓繩修復", + "carpet.rule.leadFix.desc": "修復栓繩在未加載區塊中,變得不可見的問題", + + "carpet.rule.maxEntityCollisions.name": "最大實體擠壓數量", + "carpet.rule.maxEntityCollisions.desc": "設定最大實體擠壓限制,0表示無限大", + + "carpet.rule.mergeTNT.name": "合併TNT", + "carpet.rule.mergeTNT.desc": "合併靜止的點燃的tnt實體", + + "carpet.rule.missingTools.name": "工具缺失修復", + "carpet.rule.missingTools.desc": "活塞,玻璃和海綿可以使用合適的工具更快地破壞", + + "carpet.rule.movableBlockEntities.name": "容器方塊移動", + "carpet.rule.movableBlockEntities.desc": "活塞可以推動具有容器標籤的方塊,比如箱子,漏斗", + + "carpet.rule.onePlayerSleeping.name": "單個玩家睡覺跳過夜晚", + "carpet.rule.onePlayerSleeping.desc": "只要伺服器中其中一人睡覺即可跳過全伺服器夜晚", + + "carpet.rule.optimizedTNT.name": "TNT優化", + "carpet.rule.optimizedTNT.desc": "tnt在相同位置或者在流體裡,爆炸將造成更低延遲", + + "carpet.rule.persistentParrots.name": "鸚鵡停留", + "carpet.rule.persistentParrots.desc": "鸚鵡不會離開你的肩膀,除非你受到傷害", + + "carpet.rule.placementRotationFix.name": "放置旋轉問題修復", + "carpet.rule.placementRotationFix.desc": "修復玩家放置方塊時快速轉身造成的問題", + + "carpet.rule.pushLimit.name": "自訂義推動上限", + "carpet.rule.pushLimit.desc": "自訂義活塞推動上限", + "carpet.rule.pushLimit.extra.0": "你必須在1-1024中選取一個值,預設是12", + + "carpet.rule.quasiConnectivity.name": "半連接激活開關", + "carpet.rule.quasiConnectivity.desc": "活塞,發射器和投擲器在他們上方方塊激活時是否響應", + "carpet.rule.quasiConnectivity.extra.0": "設置為true為開啟 設置為false為禁用", + + "carpet.rule.railPowerLimit.name": "自訂義動力鐵軌激活距離", + "carpet.rule.railPowerLimit.desc": "動力鐵軌激活距離能在此更改", + "carpet.rule.railPowerLimit.extra.0": "你必須在1-1024中選取一個數值", + + "carpet.rule.renewableCoral.name": "可再生珊瑚", + "carpet.rule.renewableCoral.desc": "使用骨粉可催熟珊瑚以長成珊瑚方塊結構", + + "carpet.rule.renewableSponges.name": "可再生海綿", + "carpet.rule.renewableSponges.desc": "深海守衛將在被雷擊後變成遠古深海守衛", + + "carpet.rule.rotatorBlock.name": "仙人掌扳手發射器版", + "carpet.rule.rotatorBlock.desc": "發射器中的仙人掌可以旋轉方塊(盡可能以逆時針)", + + "carpet.rule.scriptsAutoload.name": "Scarpet腳本自動加載", + "carpet.rule.scriptsAutoload.desc": "在伺服器/世界加載的時候將會自動從世界文件中加載Scarpet腳本文件", + "carpet.rule.scriptsAutoload.extra.0": "/script指令必須為啟用狀態", + + "carpet.rule.shulkerSpawningInEndCities.name": "可重生界伏蚌", + "carpet.rule.shulkerSpawningInEndCities.desc": "界伏蚌將會在終界城市中重生", + + "carpet.rule.silverFishDropGravel.name": "蠹魚鑽出方塊掉落礫石", + "carpet.rule.silverFishDropGravel.desc": "蠹魚在鑽出方塊時會掉落礫石", + + "carpet.rule.smoothClientAnimations.name": "更平暢玩家端動畫", + "carpet.rule.smoothClientAnimations.desc": "降低TPS設置時可以使玩家端動畫更加平暢", + "carpet.rule.smoothClientAnimations.extra.0": "僅在單人模式中生效,並且會降低玩家端運行速度", + + "carpet.rule.stackableShulkerBoxes.name": "可堆疊界伏盒", + "carpet.rule.stackableShulkerBoxes.desc": "空的界伏盒扔在地上時將會自動堆疊,直到堆疊為1組", + "carpet.rule.stackableShulkerBoxes.extra.0": "在物品GUI中,請使用shift鍵+單擊以移動整組界伏盒", + + "carpet.rule.summonNaturalLightning.name": "召喚閃電自然化", + "carpet.rule.summonNaturalLightning.desc": "召喚的閃電將具有自然生成時的特性", + + "carpet.rule.tntDoNotUpdate.name": "TNT放置時不會被更新", + "carpet.rule.tntDoNotUpdate.desc": "TNT在放置時不會產生任何方塊更新(不會激活)", + + "carpet.rule.tntPrimerMomentumRemoved.name": "移除TNT點燃時隨機動量", + "carpet.rule.tntPrimerMomentumRemoved.desc": "TNT點燃時的隨機動量將被移除", + + "carpet.rule.tntRandomRange.name": "TNT爆炸範圍設置", + "carpet.rule.tntRandomRange.desc": "設置TNT隨機爆炸範圍為一個固定的值,設為-1以禁用", + "carpet.rule.tntRandomRange.extra.0": "必須啟用optimizedTNT功能", + "carpet.rule.tntRandomRange.extra.1": "不能設置為負數,除了-1", + + "carpet.rule.unloadedEntityFix.name": "未加載實體修復", + "carpet.rule.unloadedEntityFix.desc": "進入或者被推入未加載區塊的實體不會消失", + + "carpet.rule.viewDistance.name": "自訂義視野", + "carpet.rule.viewDistance.desc": "改變伺服器端視野距離,設置為0以使用默認值", + "carpet.rule.viewDistance.extra.0": "你必須在0(使用伺服器默認設置)-32取一個整數", + + "carpet.rule.xpNoCooldown.name": "經驗球吸收無冷卻", + "carpet.rule.xpNoCooldown.desc": "玩家吸收經驗球不會有冷卻時間", + + "carpet.rule.portalSuffocationFix.name": "傳送門窒息修復", + "carpet.rule.portalSuffocationFix.desc": "使地獄傳送門正確地傳送實體", + "carpet.rule.portalSuffocationFix.extra.0": "傳送的實體將不再卡在黑曜石裡", + + "carpet.rule.portalCreativeDelay.name": "創造模式玩家地獄門傳送延遲", + "carpet.rule.portalCreativeDelay.desc": "創造模式下使用地獄傳送門將同生存時產生延遲", + + "carpet.rule.commandProfile.name": "遊戲性能監視器", + "carpet.rule.commandProfile.desc": "啟用/profile命令以監視遊戲性能", + "carpet.rule.commandProfile.extra.0": "/tick命令功能的部分增強版", + + "carpet.rule.commandScriptACE.name": "地毯腳本使用權限", + "carpet.rule.commandScriptACE.desc": "限制無權限的玩家執行地毯腳本", + "carpet.rule.commandScriptACE.extra.0": "無此權限的玩家無法執行/script run指令或者加載腳本", + + "carpet.rule.creativeFlyDrag.name": "創造飛行阻力系數", + "carpet.rule.creativeFlyDrag.desc": "允許使用本功能更改創造模式下飛行時的空氣阻力系數", + "carpet.rule.creativeFlyDrag.extra.0": "阻力增加會降低您的飛行速度,\n所以需要相應地調整速度,\n使用 1.0 阻力系數的時候,使用 11 的速度系數似乎與原版速度相匹配.\n本功能僅在游戏端也安裝地毯MOD(版本>=伺服器安裝版本)的情況下生效", + + "carpet.rule.creativeFlySpeed.name": "創造飛行速度系數", + "carpet.rule.creativeFlySpeed.desc": "允許使用本功能更改創造模式下飛行速度系數", + "carpet.rule.creativeFlySpeed.extra.0": "本功能僅在游戏端也安裝地毯MOD(版本>=服務器安裝版本)的情況下生效", + + "carpet.rule.creativeNoClip.name": "創造玩家無碰撞檢測", + "carpet.rule.creativeNoClip.desc": "創造玩家可以穿過方塊", + "carpet.rule.creativeNoClip.extra.0": "這個選項需要伺服器和游戏端同時支持才生效.\n僅在伺服器設置是無效的,而僅在游戏端設置的情況下\n妳可以利用壹些和活板門有關的技巧來穿墻", + + "carpet.rule.fogOff.name": "關閉霧氣", + "carpet.rule.fogOff.desc": "啟用本功能以關閉地獄與終界的霧氣", + "carpet.rule.fogOff.extra.0": "提高了可見度,但看起來很奇怪", + + "carpet.rule.forceloadLimit.name": "永加載區塊上限更改", + "carpet.rule.forceloadLimit.desc": "允許使用本功能更改/forceload命令標記的永久加載區塊上限", + + "carpet.rule.interactionUpdates.name": "放置方塊產生更新", + "carpet.rule.interactionUpdates.desc": "放置方塊是否產生更新的開關,設為false以避免產生更新", + + "carpet.rule.language.name": "語言", + "carpet.rule.language.desc": "為地毯Mod設置所需語言", + + "carpet.rule.portalSurvivalDelay.name": "生存模式地獄門傳送延遲", + "carpet.rule.portalSurvivalDelay.desc": "設置生存模式下地獄門傳送的延遲(單位:tick)", + + "carpet.rule.spawnChunksSize.name": "出生點永加載區塊半徑", + "carpet.rule.spawnChunksSize.desc": "更改出生點永加載區塊半徑", + "carpet.rule.spawnChunksSize.extra.0": "定義新半徑\n設為0以禁用出生點區塊加載", + + "carpet.rule.chainStone.name": "強化鎖鏈", + "carpet.rule.chainStone.desc": "長條鎖鏈在末端間將會相互連接", + "carpet.rule.chainStone.extra.0": "並且也會與直接連接的方塊連接\n當啟用 stick_to_all : 將無視視覺上連接狀態,使全部連接", + + "carpet.rule.piglinsSpawningInBastions.name": "豬布林堡壘生成", + "carpet.rule.piglinsSpawningInBastions.desc": "豬布林將在堡壘遺跡生成", + "carpet.rule.piglinsSpawningInBastions.extra.0": "包括 : 豬布林,蠻兵,及一些豬布獸", + + "carpet.rule.renewableBlackstone.name": "可再生黑石", + "carpet.rule.renewableBlackstone.desc": "地獄玄武岩生成機,但在下方沒有靈魂沙", + "carpet.rule.renewableBlackstone.extra.0": "..將會轉換成黑石", + + "carpet.rule.cleanLogs.name": "清除數據追蹤器", + "carpet.rule.cleanLogs.desc": "從數據追蹤器中刪除令人討厭的信息", + "carpet.rule.cleanLogs.extra.0": "在'達到最大聲音對象池247'時將不再顯示", + "carpet.rule.cleanLogs.extra.1": "在一般農場與裝置是正常顯示", + + "carpet.rule.structureBlockOutlineDistance.name": "結構方塊外框顯示距離", + "carpet.rule.structureBlockOutlineDistance.desc": "自訂義結構方塊外框顯示距離", + "carpet.rule.structureBlockOutlineDistance.extra.0": "需要客戶端支持才能正常工作", + + "carpet.rule.superSecretSetting.name": "最高機密設置", + + "carpet.rule.lightningKillsDropsFix.name": "閃電破壞掉落物修正", + "carpet.rule.lightningKillsDropsFix.desc": "閃電會破壞自閃電擊殺實體的掉落物", + "carpet.rule.lightningKillsDropsFix.extra.0": "設置成true來防止掉落物被閃電破壞", + "carpet.rule.lightningKillsDropsFix.extra.1": "修正[MC-206922](https://bugs.mojang.com/browse/MC-206922)", + + "carpet.rule.pingPlayerListLimit.name": "玩家ping列表數量限制", + "carpet.rule.pingPlayerListLimit.desc": "自訂義伺服器ping列表(多人遊戲菜單)玩家列表樣本限制", + + "carpet.rule.structureBlockIgnored.name": "結構方塊變更無視方塊", + "carpet.rule.structureBlockIgnored.desc": "變更被結構方塊無視的方塊", + + "carpet.rule.structureBlockLimit.name": "結構方塊上限更改", + "carpet.rule.structureBlockLimit.desc": "自訂義結構方塊每個方位距離上限", + "carpet.rule.structureBlockLimit.extra.0": "警告 : 必須是永久的才能正確加載", + "carpet.rule.structureBlockLimit.extra.1": "建議設置'結構方塊變更無視方塊'成空氣(air)", + "carpet.rule.structureBlockLimit.extra.2": "當儲存大量結構時", + "carpet.rule.structureBlockLimit.extra.3": "需要客戶端玩家編輯結構方塊", + "carpet.rule.structureBlockLimit.extra.4": "當渲染很長的結構時,\n'結構方塊外框顯示距離'將會是必要開啟的", + + + + "carpet.category.bugfix": "BUG修復", + "carpet.category.survival": "生存用", + "carpet.category.creative": "創造用", + "carpet.category.experimental": "實驗用", + "carpet.category.optimization": "優化", + "carpet.category.feature": "特性", + "carpet.category.command": "指令", + "carpet.category.tnt": "TNT", + "carpet.category.dispenser": "發射器", + "carpet.category.scarpet": "scarpet腳本程式語言", + "carpet.category.client": "玩家端", + + "carpet.settings.command.browse_categories": "瀏覽分類", + "carpet.settings.command.version": "版本", + "carpet.settings.command.list_all_category": "列出所有 %s 選項", + "carpet.settings.command.current_settings_header": "目前啟用的 %s 選項", + "carpet.settings.command.switch_to": "切換至%s", + "carpet.settings.command.unknown_rule": "未知的規則選項", + "carpet.settings.command.current_from_file_header": "目前啟用的%s選項,從%s中讀取得", + "carpet.settings.command.mod_settings_matching": "%s中符合\"%s\"的選項:", + "carpet.settings.command.all_mod_settings": "所有的%s選項", + "carpet.settings.command.tags": "分類", + "carpet.settings.command.change_permanently": "設為預設", + "carpet.settings.command.change_permanently_tooltip": "點擊此按鈕以使此選項保存於%s,重啟後依舊生效", + "carpet.settings.command.default_set": "規則%s將會被默認設置為%s", + "carpet.settings.command.default_removed": "Rule %s will no longer be set on restart", + "carpet.settings.command.current_value": "當前的值" + +} diff --git a/src/main/resources/assets/carpet/scripts/ai_tracker.sc b/src/main/resources/assets/carpet/scripts/ai_tracker.sc new file mode 100644 index 0000000..d3ccd7c --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/ai_tracker.sc @@ -0,0 +1,590 @@ +print_info() -> print(' +ai_tracker allows to display +some extra information about +various entities AI activity + +WIP - more options and settings will be +added in the future + +Current supported actions + - current selected paths for all living entities + - movement speeds per block and coordinate + - item pickup range + - portal cooldown timers + - health + - buddy villager detection for villagers + - hostile detection for villagers + - iron golem spawning for villagers + - breeding info for villagers + +Settings you may want to change + - toggle boxes: hides/shows large boxes and spheres + - update_frequency: changes update speed + - clear: removes all options + - transparency: default opacity of shapes, 8 for start +'); + + +global_functions = { + 'villager_iron_golem_spawning' -> { + 'villager' -> [ + _(arg) -> _(e) -> ( + abnoxious_visuals = []; + visuals = []; + half_width = e~'width'/2; + villager_height = e~'height'; + __create_box(abnoxious_visuals, e, + [-8,-6,-8], + [9,7,9], + 0x00dd0000, 'golem spawning', true + ); + __create_box(abnoxious_visuals, e, + [-16-half_width,-16,-16-half_width], + [16+half_width,16+height,16+half_width], + 0xdddddd00, 'golem detection', false + ); + + last_seen = 0; + entry = query(e, 'brain', 'golem_detected_recently'); + if (entry, last_seen = entry:1); + + if (last_seen, + mobs = query(e, 'brain', 'mobs'); + if(mobs, for(filter(mobs, _ ~'type' == 'iron_golem'), + w = _~'width'/2; + __outline_mob(visuals, _, 0xee000000, 'in range', 0.5); + visuals+=['line', global_duration, 'from', pos(e), 'to', pos(_), 'color', 0xddddddff]; + )); + ); + + + entry = query(e, 'brain', 'last_slept'); + slept = system_info('world_time')-entry; + last_slept = format(if(entry == null, 'rb never', if(slept < 24000, 'e ', 'y ')+slept)); + + labels = [ + ['golem timer', 'golem:', format(if(last_seen,'rb ','eb ')+last_seen )], + ['sleep tracker', 'slept:', last_slept], + ['attempt', 'attempt in:', format(if( slept < 24000 && last_seen==0,'yb ','gi ')+ (100-(system_info('world_time')%100)))], + ]; + [visuals, abnoxious_visuals, labels]; + ) + , + null + ] + }, + 'villager_buddy_detection' -> { + 'villager' -> [ + _(arg) -> _(e) -> ( + visuals = []; + abnoxious_visuals = []; + half_width = e~'width'/2; + height = e~'height'; + __create_box(abnoxious_visuals, e, + [-10-half_width,-10,-10-half_width], + [10+half_width,10+height,10+half_width], + 0x65432100, 'buddy detection', false + ); + current_id = e~'id'; + buddies = entity_area('villager', e, 10, 10, 10); + nb = length(buddies); + for (filter(buddies, _~'id' != current_id), + visuals+=['line', global_duration, 'from', pos(e), 'to', pos(_), 'color', 0xffff00ff]; + ); + [visuals, abnoxious_visuals, [['vill_seen', 'buddies: ', format(if(nb==3, 'eb ', nb > 3, 'yb ', 'rb ' )+nb)]]]; + ), + , + null + ] + }, + 'item_pickup' -> { + '*' -> [ + _(arg) -> _(e) -> ( + visuals = []; + abnoxious_visuals = []; + e_half_width = e~'width'/2; + e_height = e~'height'; + [box_from, box_to, color] = if (e~'type' == 'player', + + if ((ride = e~'mount') != null, + rpos = pos(ride)-pos(e); + r_half_width = ride~'width'/2; + r_height = ride~'height'; + [ [ + -1+min(-e_half_width, rpos:0-r_half_width), + min(0, rpos:1), + -1+min(-e_half_width, rpos:2-r_half_width) + ], [ + 1+max(e_half_width, rpos:0+r_half_width), + max(e_height, rpos:1+r_height), + 1+max(e_half_width, rpos:2+r_half_width) + ], 0xffaa0000 + ] + , + [ + [ -1-e_half_width, -0.5, -1-e_half_width ], + [ 1+e_half_width, e_height+0.5, 1+e_half_width ], + 0xffaa0000 + ] + ) + , + + [ + [ -1-e_half_width, 0, -1-e_half_width ], + [ 1+e_half_width, e_height, 1+e_half_width ], + 0xffff0000 + ] + ); + box_ctr = (box_from+box_to)/2; + box_range = box_ctr-box_from; + + __create_box(abnoxious_visuals, e, + box_from, + box_to, + color, 'item range', false + ); + for (filter(entity_area('item', pos(e)+box_ctr, box_range), pos(_) != pos(e)), + visuals+=['line', global_duration, 'from', pos(e)+[0,1,0], 'to', pos(_)+[0,0,0], 'color', 0xffff00ff] + ); + + + [visuals, abnoxious_visuals, []] + ), + , + null + ] + }, + + 'villager_hostile_detection' -> { + 'villager' -> [ + _(hostile) -> _(e, outer(hostile)) -> ( + visuals = []; + abnoxious_visuals = []; + tags = []; + if (has(global_hostile_to_villager:hostile), + abnoxious_visuals += ['sphere', global_duration, 'center', [0,0,0], 'radius', global_hostile_to_villager:hostile, 'follow', e, + 'color', 0x88000055, 'fill', 0x88000000+global_opacity] + ); + mobs = query(e, 'brain', 'visible_mobs'); + if (mobs, + mob = query(e, 'brain', 'nearest_hostile'); + id = -1; + + if (mob, + __outline_mob(visuals, mob, 0xff000000, 'danger', 0.5); + visuals+=['line', global_duration, 'from', pos(e)+[0,e~'eye_height',0], 'to', pos(mob)+[0,mob~'eye_height',0], 'color', 0xff0000ff, 'line', 5]; + id = mob~'id'; + ); + other_hostile_mobs = filter(mobs, __is_hostile(e, _) && _~'id' != id); + for (other_hostile_mobs, + __outline_mob(visuals, _, 0xaa880000, 'detected', 0.7); + ); + tags += ['detection', 'hostile:', format( + if (mob, 'rb '+mob, other_hostile_mobs, 'y detected', 'e peaceful') + )]; + , + tags += ['detection', 'hostile:', format('e peaceful')] + ); + [visuals, abnoxious_visuals, tags]; + ), + , + null + ] + }, // ☖ ☽ ♨ ♡ + 'villager_breeding' -> { + 'villager' -> [ + _(arg) -> _(e) -> ( + visuals = []; + abnoxious_visuals = []; + + bed = query(e, 'brain', 'home'); + if (bed, + [dim, bed_pos] = bed; + same_dim = dim == e~'dimension'; + visuals += ['line', global_duration, + 'from', pos(e)+[0,1,0], 'to', if(same_dim, bed_pos+[0.5,0.5626,0.5], pos(e)+[0,3,0]), 'color', 0xffffaaff, 'line', 5]; + if (same_dim, + visuals += ['box', global_duration, 'from', bed_pos, 'to', bed_pos+[1, 0.5626, 1], 'color', 0xffffaacc, 'fill', 0xffffaa88] + ); + + ); + + food = __compute_food(e); + portions = floor(food / 12); + + breeding_age = e~'breeding_age'; + + + [visuals, abnoxious_visuals, [ + ['hasbed', 'bed: ', format(if(bed, 'be has home', 'br no home'))], // ☖ ☽ ♨ ♡ + ['hasfood', 'food: ', format(if(food, 'be '+portions+' portions', 'br 0 portions'))], + ['breeding', 'timer: ',format(if(!breeding_age, 'be ', 'br ')+breeding_age)], + + ]]; + ), + , + _(arg) -> _(e, i) -> ( + item = i:0; + print(item); + if (item == 'rotten_flesh', + loop(inventory_size(e), + inventory_set(e, _, null); + ); + , + item ~ '^\\w+_bed$', + print('got bed') + ) + + ) + ] + }, + + 'velocity' -> { + '*' -> [ + _(arg) -> _(e) -> ( + id = e~'id'; + labels = []; + if (has(global_entity_positions:id), + cpos = pos(e); + ppos = global_entity_positions:id; + dpos = ppos-cpos; + dpos = dpos*20/global_interval; + if (dpos != [0, 0, 0], + labels += ['speed', 'speed: ', str('%.3f',sqrt(reduce(dpos, _a+_*_, 0)))]; + labels += ['xvel', 'x speed:', str('%.3f',dpos:0)]; + labels += ['yvel', 'y speed:', str('%.3f',dpos:1)]; + labels += ['zvel', 'z speed:', str('%.3f',dpos:2)]; + ) + ); + global_entity_positions:id = pos(e); + path = e ~ 'path'; + visuals = []; + if (path, for(path, __mark_path_element(_, visuals))); + [[], [], labels]; + ), + , + null + ] + }, + + 'portal_cooldown' -> { + '*' -> [ + _(arg) -> _(e) -> ( + portal_cooldown = e~'portal_cooldown'; + if (portal_cooldown, + [[], [], [['portal', 'portal cooldown:', portal_cooldown]]] + , + [[], [], []] + ) + ), + , + null + ] + }, + + 'despawn_timer' -> { + '*' -> [ + _(arg) -> _(e) -> ( + despawn_timer = e~'despawn_timer'; + if (despawn_timer, + [[], [], [['despawn', 'despawn timer:', despawn_timer]]] + , + [[], [], []] + ) + ), + , + null + ] + }, + + 'health' -> { + '*' -> [ + _(arg) -> _(e) -> ( + health = e~'health'; + [[], [], if (health != null, [['health', 'health:', e~'health']], [])] + ), + , + null + ] + }, + + 'pathfinding' -> { + '*' -> [ + _(arg) -> _(e) -> ( + path = e ~ 'path'; + visuals = []; + if (path, for(path, __mark_path_element(_, visuals))); + [visuals, [], []]; + ), + , + null + ] + }, + + 'xpstack' -> { + 'experience_orb' -> [ + _(arg) -> _(orb) -> ( + tag = query(orb, 'nbt'); + ct = tag:'Count'; + [[], [], if (ct > 1, [['stack', ct]],[]) ] + ), + , + null + ] + }, + + 'drowning' -> { + 'zombie' -> [ + _(arg) -> _(entity) -> ( + w = query(entity, 'width')/2; + e = query(entity, 'eye_height'); + from = [-w, e-0.111,-w]; + to = [w, e-0.111, w]; + data = query(entity, 'nbt'); + iwt = data:'InWaterTime'; + messages = if( + 0 < iwt < 600, + [['drown','drowning in:', 600-iwt]], + dt = data:'DrownedConversionTime'; dt > 0, + [['drown','drowned in:', dt]], + []); + [[ ['box', global_duration, 'from', from, 'to', to, 'follow', entity, 'color', 0x00bbbbff, 'fill', 0x00bbbb22]],[],messages], + ) + , + null + ] + } +}; + +global_hostile_to_villager = { + 'drowned' -> 8, + 'evoker'-> 12, + 'husk' -> 8, + 'illusioner' -> 12, + 'pillager' -> 15, + 'ravager' -> 12, + 'vex' -> 8, + 'vindicator' -> 10, + 'zoglin' -> 10, + 'zombie' -> 8, + 'zombie_villager' -> 8 +}; + + +__config() ->{ + 'commands'->{ + ''->'print_info', + 'clear'->'clear', + 'toggle_boxes'->_()->global_display_boxes = !global_display_boxes, + ''->['__toggle',null], + 'villager '->_(d)->__toggle('villager_'+d,null), + 'villager hostile_detection '->_(h)->__toggle('villager_hostile_detection',h), + 'update_frequency '->_(ticks)->(global_interval = ticks;global_duration = ticks + 2), + 'transparency '->_(alpha)->global_opacity = alpha + }, + 'arguments'->{ + 'display'->{'type'->'term','options'->['item_pickup','velocity','portal_cooldown','despawn_timer','health','pathfinding','xpstack','drowning']}, + 'aspect'->{'type'->'term','options'->['iron_golem_spawning','buddy_detection','hostile_detection','breeding']}, + 'ticks'->{'type'->'int','min'->0,'max'->100}, + 'alpha'->{'type'->'int','min'->0,'max'->255}, + 'hostile'->{'type'->'term','options'->keys(global_hostile_to_villager)} + } +}; + +global_duration = 12; +global_interval = 10; + +global_opacity = 8; + +global_display_boxes = true; + +global_range = 48; + +// list of triples - [entity_type, feature, callback] +global_active_functions = []; +global_feature_switches = {}; +global_tracker_running = false; + +global_entity_positions = {}; + +global_villager_food = { + 'bread' -> 4, + 'potato' -> 1, + 'carrot' -> 1, + 'beetroot' -> 1 +}; + +__compute_food(villager) -> +( + food = 0; + for (filter(inventory_get(villager), _), + val = global_villager_food:(_:0); + if (val, food += val*_:1); + ); + food; +); + +__outline_mob(visuals, e, color, caption, offset) -> +( + w = e~'width'/2; + visuals += ['box', global_duration, 'from', [-w,0,-w], 'to', [w, e~'height', w], 'follow', e, 'color', color+120, 'fill', color+global_opacity]; + visuals += ['label', global_duration, 'pos', [0, e~'height'+offset, 0], 'text', caption, 'follow', e, 'color', color+255]; +); + +__create_box(visuals, e, from, to, color, caption, discrete) -> +( + visuals += ['box', global_duration, 'from', from, 'to', to, + 'follow', e, 'snap', if(discrete, 'dxdydz', 'xyz'), 'color', color+255, 'fill', color+global_opacity]; + top_center = (from+to)/2; + top_center:1 = to:1+1; + visuals += ['line', global_duration, 'from', to, 'to', to+[1,2,1], + 'follow', e, 'snap', if(discrete, 'dxdydz', 'xyz'), 'color', color+255, 'line', 10]; + visuals += ['label', global_duration, 'pos', to+[1,2,1], 'text', caption, + 'follow', e, 'snap', if(discrete, 'dxdydz', 'xyz'), 'color', color+255 ]; + +); + +__mark_path_element(path_element, visuals) -> +( + [block, type, penalty, completed] = path_element; + color = if (penalty, 0x88000000, 0x00880000); + visuals += ['box', global_duration, + 'from', pos(block), 'to', pos(block)+[1,0.1+0.01*penalty,1], 'color', color+255, 'fill', color+global_opacity]; + if (penalty, + visuals += ['label', global_duration, 'pos', pos(block)+0.5, 'text', 'penalty', 'value', penalty, 'color', color+255] + ) +); + +__is_hostile(v, m) -> +( + mob = m~'type'; + has(global_hostile_to_villager:mob) && (sqrt(reduce(pos(v)-pos(m), _a+_*_ , 0)) <= global_hostile_to_villager:mob) +); + + +__toggle(feature, arg) -> +( + if (has(global_feature_switches:feature) && global_feature_switches:feature == arg, + // disable + global_active_functions = filter(global_active_functions, _:1 != feature); + delete(global_feature_switches:feature); + , + //enable + // clean previous + global_active_functions = filter(global_active_functions, _:1 != feature); + + for(pairs(global_functions:feature), + global_active_functions += [_:0, feature, if (_:1:0, call(_:1:0, arg), null), if (_:1:1, call(_:1:1, arg), null)] + ); + global_feature_switches:feature = arg; + if (!global_tracker_running, + global_tracker_running = true; + __tick_tracker(); + ) + ); + __reset_interaction_types(); + null; +); + +global_interaction_types = {}; + +__reset_interaction_types() -> +( + global_interaction_types = {}; + for (global_feature_switches, feature = _; + for ( filter(keys(global_functions:feature), global_functions:feature:_:1 != null ), + global_interaction_types += _; + if (_ == '*', return()); + ) + ); +); + +__on_player_interacts_with_entity(player, entity, hand) -> +( + if (hand == 'mainhand' && global_interaction_types && (has(global_interaction_types:(entity~'type')) || has(global_interaction_types:'*')), + for (global_active_functions, + if (_:3 != null && (_:0 == '*' || _:0 == entity~'type'), + call(_:3, entity, player~'holds') + ) + ) + ) +); + +clear() -> +( + global_active_functions = []; + global_feature_switches = {}; + null +); + + + +__tick_tracker() -> +( + if (!global_active_functions, + global_tracker_running = false; + return() + ); + p = player(); + in_dimension(p, + for (entity_area('valid', p, global_range, global_range, global_range), + __handle_entity(_) + ) + ); + schedule(global_interval, '__tick_tracker'); +); + +global_entity_anchors = { + 'experience_orb' -> 'xyz', + 'player' -> 'xyz' +}; + +global_jitter = {'experience_orb'}; + +__handle_entity(e) -> +( + entity_type = e ~ 'type'; + shapes_to_display = []; + abnoxious_to_display = []; + labels_to_add = []; + for ( global_active_functions, + if (_:2 != null && (_:0 == '*' || _:0 == entity_type), + [shapes, abnoxious_shapes, labels] = call(_:2, e); + put(shapes_to_display, null, shapes, 'extend'); + if (global_display_boxes, put(abnoxious_to_display, null, abnoxious_shapes, 'extend');); + put(labels_to_add, null, labels, 'extend'); + ) + ); + + put(shapes_to_display, null, abnoxious_to_display, 'extend'); + + if (labels_to_add, + base_height = 0; + etype = e~'type'; + eid = e~'id'; + snap = global_entity_anchors:etype || 'dxydz'; + base_pos = if(snap == 'xyz', [0, e~'height'+0.3, 0], [0.5, e~'height'+0.3, 0.5]); + if (has(global_jitter:etype), + offset = ([(eid % 7)/7,(eid % 13)/13, (eid % 23)/23]-0.5)/2; + base_pos = base_pos+offset; + ); + for (labels_to_add, + if (length(_) == 2, + [label, text] = _; + shapes_to_display += [ + 'label', global_duration, 'text', label, 'value', text, + 'pos', base_pos, 'follow', e, 'height', base_height, 'snap', snap]; + , + [label, annot, value] = _; + shapes_to_display += [ + 'label', global_duration, 'text', format('gi '+annot), 'pos', base_pos, + 'follow', e, 'height', base_height, 'align', 'right', 'indent', -0.2, 'snap', snap]; + shapes_to_display += [ + 'label', global_duration, 'text', label,'value', value, + 'pos', base_pos, 'follow', e, 'height', base_height, 'align', 'left', 'snap', snap]; + ); + base_height += 1; + ); + ); + draw_shape(shapes_to_display); +); diff --git a/src/main/resources/assets/carpet/scripts/camera.sc b/src/main/resources/assets/carpet/scripts/camera.sc new file mode 100644 index 0000000..63cc6c4 --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/camera.sc @@ -0,0 +1,688 @@ +usage() -> 'camera: scarpet app. +------------------- + "/camera start" - Set the starting point, resetting the path + + + "/camera add " - Add a point to the end, secs later + "/camera prepend " - Prepend the start secs before + "/camera clear" - Remove entire path + "/camera select" - .. a point or just punch it, to select it + "/camera place_player - Move player to the selected point + "/camera move" - Move the selected point to players location + "/camera duration " - Set new selected path duration + "/camera split_point" - Split selected path in half. + "/camera delete_point" - Remove current key point + "/camera trim_path" - Remove all key points from selected up + + "/camera save_as " + "/camera load " + - Store and load paths from world saves /scripts folder + + "/camera interpolation < linear | gauss | catmull_rom | gauss >" + Select interpolation between points: + - catmull_rom: Catmull-Rom interpolation (default). + smooth path that goes through all points. + - linear: straight paths between points. + - gauss: automatic smooth transitions. + - gauss : custom fixed variance + (in seconds) for special effects. + gauss makes the smoothest path, + but treating points as suggestions only + + "/camera repeat " - + Repeat existing points configuration n-times + using seconds to link path ends + + "/camera stretch " - + Change length of the entire path + from 25 -> 4x faster, to 400 -> 4x slower, + + "/camera transpose" - + Move entire path with the start at players position. + + "/camera play : Run the path with the player + use "sneak" to stop it prematurely + run /camera hide and F1 to clear the view + + "/camera show": Show current path particles + color of particles used is different for different players + "/camera hide": Hide path display + "/camera prefer_smooth_play": + Eat CPU spikes and continue as usual + "/camera prefer_synced_play": + After CPU spikes jump to where you should be +'; + +__config() -> { + 'commands' -> { + '' -> _() -> print(usage()), + '' -> '_call', + 'add ' -> 'add', + 'prepend ' -> 'prepend', + 'duration ' -> 'duration', + 'save_as ' -> 'save_as', + 'load ' -> 'load', + 'interpolation ' -> ['interpolation', null, true], + 'interpolation gauss' -> ['interpolation', 'gauss', null, true], + 'interpolation gauss ' -> _(float) -> interpolation('gauss', float, true), + 'repeat ' -> 'repeat', + 'stretch ' -> 'stretch' + }, + 'arguments'->{ + 'seconds'->{'type'->'float', 'min' -> 0.01, 'suggest'->[]}, + 'last_delay'->{'type'->'float','min' -> 0.01, '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; +global_player = null; + +global_player_eye_offset = map(range(5), 0); + +global_showing_path = false; +global_playing_path = false; + +global_needs_updating = false; +global_selected_point = null; +global_markers = null; + +global_particle_density = 100; + +global_color_a = null; +global_color_b = null; + +global_path_precalculated = null; + +// starts the path with current player location +start() -> _start_with( _() -> [ [_camera_position(), 0, 'sharp'] ] ); + +// start path with customized initial points selection +_start_with(points_supplier) -> +( + p = player(); + global_player = str(p); + global_player_eye_offset = [0, p~'eye_height',0,0,0]; + global_dimension = p~'dimension'; + + code = abs(hash_code(str(p))-123456); + global_color_a = str('dust %.1f %.1f %.1f 1',(code%10)/10,(((code/10)%10)/10), (((code/100)%10)/10) ); // 'dust 0.1 0.9 0.1 1', + global_color_b = str('dust %.1f %.1f %.1f 1',(((code/1000)%10)/10),(((code/10000)%10)/10), (((code/100000)%10)/10) );// 'dust 0.6 0.6 0.6 1' + + global_points = call(points_supplier); + global_selected_point = null; + _update(); + show(); + print(str('Started path at %.1f %.1f %.1f', p~'x', p~'y', p~'z')); +); + +// gets current player controlling the path, or fails +_get_player() -> +( + if (!global_player, exit('No player selected')); + p = player(global_player); + if (p == null, exit('Player logged out')); + p; +); + +// clears current path +clear() -> +( + global_points = []; + global_dimension = null; + global_player = null; + global_selected_point = null; + global_showing_path = false; + global_playing_path = false; + _update(); + print('Path cleared'); +); + +// camera position for the player sticks out of their eyes +_camera_position() -> (_get_player()~'location' + global_player_eye_offset); + +// path changed, now notify all processes that they need to update; +_update() -> +( + global_path_precalculated = null; + global_playing_path = false; + global_needs_updating = true; +); + +// ensures that the current execution context can modify the path +_assert_can_modify_path() -> +( + if (!global_points, exit('Path is not setup for current player')); + if (!global_showing_path, exit('Turn on path showing to edit key points')); + if(!global_player || !global_dimension || _get_player()~'dimension' != global_dimension, + exit('Player not in dimension')); +); + +// make sure selected point is correct +_assert_point_selected(validator) -> +( + _assert_can_modify_path(); + if (!call(validator, global_selected_point), exit('No appropriate point selected')); +); + +//select a custom interpolation method +interpolation(method, option, verbose) -> +( + _prepare_path_if_needed() -> _prepare_path_if_needed_generic(); + // each supported method needs to specify its _find_position_for_point to trace the path + // accepting segment number, and position in the segment + // or optionally _prepare_path_if_needed, if path is inefficient to compute point by point + global_interpolator = if ( + method == 'linear', '_interpolator_linear', + method == 'catmull_rom', '_interpolator_cr', + method == 'gauss' && option == null, _(s, p) -> _interpolator_gauB(s, p, 0), + method == 'gauss', + variance = round(60*option); + _(s, p, outer(variance)) -> _interpolator_gauB(s, p, variance); + ); + _update(); + if(verbose, print('Interpolation changed to '+method + if(option, ' '+option, ''))); +); +interpolation('catmull_rom', null, false); + +// adds a point to the end of the path with delay in seconds +add(delay) -> +( + _assert_can_modify_path(); + //mode is currently unused, run_path does always sharp, gauss interpolator is always smooth + // but this option could be used could be used at some point by more robust interpolators + _add_path_segment(_camera_position(), round(60*delay), 'smooth', true); + _update(); + print(_get_path_size_string()); +); + +// prepends the path with a new starting point, with a segment of specified delay +prepend(delay) -> +( + _assert_can_modify_path(); + _add_path_segment(_camera_position(), round(60*delay), 'smooth', false); + _update(); + print(_get_path_size_string()); +); + +// repeats existing points seveal times, using last section delay (seconds) to join points +repeat(times, last_section_delay) -> +( + if (err = _is_not_valid_for_motion(), exit(err)); + positions = map(global_points, _:0); + modes = map(global_points, _:(-1)); + durations = map(global_points, global_points:(_i+1):1 - _:1 ); + durations:(-1) = round(60*last_section_delay); + loop(times, + loop( length(positions), + _add_path_segment(copy(positions:_), durations:_, modes:_, true) + ) + ); + _update(); + print(_get_path_size_string()); +); + +//stretches or shrinks current path to X percent of what it was before +stretch(percentage) -> +( + if (err = _is_not_valid_for_motion(), exit(err)); + ratio = percentage/100; + previous_path_length = global_points:(-1):1; + for(global_points, _:1 = _:1*ratio ); + _update(); + print(str('path %s from %.2f to %.2f seconds', + if(ratio<1,'shortened','extended'), + previous_path_length/60, + global_points:(-1):1/60 + )) +); + +// moves current selected point to player location +move() -> +( + _assert_point_selected(_(p) -> p != null); + new_position = _camera_position(); + new_position:(-2) = __adjusted_rot( + global_points:global_selected_point:0:(-2), + new_position:(-2) + ); + global_points:global_selected_point:0 = new_position; + _update(); +); + +// chenges duration of the current selected segment to X seconds +duration(amount) -> +( + _assert_point_selected(_(p) -> p); // skips nulls and 0 - starting point + duration = number(amount); + new_ticks = round(duration * 60); + if (new_ticks < 10, return()); + previous_ticks = global_points:global_selected_point:1-global_points:(global_selected_point-1):1; + delta = new_ticks - previous_ticks; + // adjust duration of all points after that. + for (range(global_selected_point, length(global_points)), + global_points:_:1 += delta; + ); + _update(); + print(_get_path_size_string()); +); + +// deletes current keypoint without changing the path length +delete_point() -> +( + _assert_point_selected(_(p) -> p != null); + if (length(global_points) < 2, clear(); return()); + if (global_selected_point == 0, global_points:1:1 = 0); + global_points = filter(global_points, _i != global_selected_point); + if (global_selected_point >= length(global_points), global_selected_point = null); + _update(); + print(_get_path_size_string()); +); + +// splits current selected segment in half by adding a keypoint in between +split_point() -> +( + _assert_point_selected(_(p) -> p); // skips nulls and 0 - starting point + current_time = global_points:global_selected_point:1; + previous_time = global_points:(global_selected_point-1):1; + segment_duration = current_time-previous_time; + put( + global_points, + global_selected_point, + [ + _get_path_at(global_selected_point-1, previous_time, segment_duration/2), + previous_time+segment_duration/2, + global_points:global_selected_point:2 + ], + 'insert' + ); + _update(); + print(_get_path_size_string()); +); + +// removes all points in the path from the current point +trim_path() -> +( + _assert_point_selected(_(p) -> p != null); + global_points = slice(global_points, 0, global_selected_point); + global_selected_point = null; + _update(); + print(_get_path_size_string()); +); + +// moves entire camera path keeping the angles to player position being in the starting point +transpose() -> +( + _assert_can_modify_path(); + shift = pos(_get_player())-slice(global_points:0:0, 0, 3); + shift += 0; shift += 0; + shift = shift + global_player_eye_offset; + for(global_points, _:0 = _:0 + shift); + _update(); + print(_get_path_size_string()); +); + +// selects either a point of certain number (starting from 1), or closest point +select(num) -> +( + if (!global_points, return()); + if (!global_showing_path, return()); + p = _get_player(); + num = (num+1000*(length(global_points)+1)) % (length(global_points)+1); + selected_point = if (num, num-1, + _closest_point_to_center( + p~'pos'+[0, p~'eye_height', 0], + map(global_points, slice(_:0, 0, 3)) + ) + ); + global_selected_point = if (global_selected_point == selected_point, null, selected_point); + global_needs_updating = true; +); + +// player can also punch the mannequin to select/deselect it +__on_player_attacks_entity(p, e) -> +( + if (e~'type' == 'armor_stand' && query(e, 'has_tag', '__scarpet_marker_camera') && global_markers, + for (global_markers, + if(_==e, + global_selected_point = if (global_selected_point == _i, null, _i); + global_needs_updating = true; + return(); + ) + ); + ); +); + +// adds new segment to the path +_add_path_segment(vector, duration, mode, append) -> +( + if ( (['sharp','smooth'] ~ mode) == null, exit('use smooth or sharp point')); + [v, segment_time, m] = global_points:(if(append, -1, 0)); + vector:(-2) = __adjusted_rot(v:(-2), vector:(-2)); + if (append, + global_points += [vector, segment_time+duration, mode]; + , + new_points = [[vector, 0, mode]]; + for (global_points, + _:1 += duration; + new_points += _; + ); + global_points = new_points; + ); + null; +); + +// adjusts current rotation so we don't spin around like crazy +__adjusted_rot(previous_rot, current_rot) -> +( + while( abs(previous_rot-current_rot) > 180, 1000, + current_rot += if(previous_rot < current_rot, -360, 360) + ); + current_rot +); + +// returns current path size blurb +_get_path_size_string() -> +( + if (!_is_not_valid_for_motion(), + str('%d points, %.1f secs', length(global_points), global_points:(-1):1/60); + , + 'Path too small to run'; + ); +); + +// checks if the current path is valid for motion +_is_not_valid_for_motion() -> +( + if(!global_points, return('Path not defined yet')); + if(length(global_points)<2, return('Path not complete - add more points')); + if(!global_dimension || _get_player()~'dimension' != global_dimension, return('Wrong dimension')); + false +); + +// grabs position of a player for a given segment, which segment starts at start point, and offset by index points +_get_path_at(segment, start, index) -> +( + v = global_path_precalculated:(start+index); + if(v == null, + v = call(global_interpolator, segment, index); + global_path_precalculated:(start+index) = v + ); + v +); + +// squared euclidean distance between two points +_distsq(vec1, vec2) -> reduce(vec1 - vec2, _a + _*_, 0); + +//finds index of the closest point from a list to the center point +_closest_point_to_center(center, points) -> +( + reduce(points, + d = _distsq(_, center); if( d +( + _get_path_size_string(); + if (global_showing_path, return ()); + global_showing_path = true; + global_needs_updating= false; + _create_markers() -> + ( + map(global_points || [], + is_selected = global_selected_point != null && _i == global_selected_point; + caption = if (_i == 0, '1: Start', str('%d: %.1fs', _i+1, global_points:_i:1/60); ); + if (is_selected && _i > 0, + caption += str(' (%.1fs current segment)', (global_points:_i:1 - global_points:(_i-1):1)/60); + ); + marker = create_marker(caption, _:0, 'observer'); + if (is_selected, + modify(marker,'effect','glowing',72000, 0, false, false); + ); + marker + ); + ); + __show_path_tick() -> + ( + if (_is_not_valid_for_motion(), return()); + _prepare_path_if_needed(); + loop(global_particle_density, + segment = floor(rand(length(global_points)-1)); + particle_type = if ((segment+1) == global_selected_point, global_color_a, global_color_b); //'dust 0.1 0.9 0.1 1', 'dust 0.6 0.6 0.6 1'); + start = global_points:segment:1; + end = global_points:(segment+1):1; + index = floor(rand(end-start)); + [x, y, z] = slice(_get_path_at(segment, start, index), 0, 3); + particle(particle_type, x, y, z, 1, 0, 0) + ); + null + ); + + task( _() -> ( + global_markers = _create_markers(); + on_close = ( _() -> ( + for(global_markers, modify(_,'remove')); + global_markers = null; + global_showing_path = false; + )); + + loop(7200, + if(!global_showing_path, break()); + _get_player(); + if (global_needs_updating, + global_needs_updating = false; + for(global_markers, modify(_,'remove')); + global_markers = _create_markers(); + ); + __show_path_tick(); + sleep(100, call(on_close)); + ); + call(on_close); + )); + null; +); + +// hides path display +hide() -> +( + if (global_showing_path, + global_showing_path = false; + ); +); + +// 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)); + _prepare_path_if_needed(); + if (!_get_player() || _get_player()~'dimension' != global_dimension, exit('No player in dimension')); + task( _() -> ( + if (global_playing_path, // we don't want to join_task not to lock it just in case. No need to panic here + global_playing_path = false; // cancel current path rendering + sleep(1500); + ); + showing_path = global_showing_path; + hide(); + sleep(1000); + sound('ui.button.click', pos(_get_player()), 8, 1); // to synchro with other clips + sleep(1000); // so particles can discipate + global_playing_path = true; + mspt = 1000 / 60; + start_time = time(); + very_start = start_time; + point = 0; + p = _get_player(); + try ( + loop( length(global_points)-1, segment = _; + start = global_points:segment:1; + end = global_points:(segment+1):1; + loop(end-start, + if (p~'sneaking', global_playing_path = false); + if (!global_playing_path, throw()); + v = _get_path_at(segment, start, _)-global_player_eye_offset; + modify(p, 'location', v); + point += 1; + end_time = time(); + sleep(); + 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() + ) + ) + ); + ); + sleep(1000); + global_playing_path = false; + if (showing_path, show()); + )); + null; +); + +// moves player to a selected camera position +place_player() -> +( + _assert_point_selected(_(p) -> p != null); + modify(_get_player(), 'location', global_points:global_selected_point:0 - global_player_eye_offset); +); + +// prepares empty path to fit new points +_prepare_path_if_needed_generic() -> +( + if(!global_path_precalculated, global_path_precalculated = map(range(global_points:(-1):1), null)) +); + +// linear interpolator +_interpolator_linear(segment, point) -> +( + [va, start, mode_a] = global_points:segment; + [vb, end, mode_b] = global_points:(segment+1); + section = end-start; + dt = point/section; + dt*vb+(1-dt)*va +); + +// normal distribution should look like that +//(1/sqrt(2*pi*d*d))*euler^(-((x-miu)^2)/(2*d*d)) +// but we will be normalizing anyways, so who cares +_norm_prob(x, miu, d) -> euler^(-((x-miu)^2)/(2*d*d)); + +//gauB interpolator +_interpolator_gauB(from_index, point, deviation) -> +( + components = []; + path_point = global_points:from_index:1; + try( + for(range(from_index+1, length(global_points)), + [v,ptime,mode] = global_points:_; + dev = if (deviation > 0, deviation, + devs = []; + if (_+1 < length(global_points), devs += global_points:(_+1):1-ptime); + if (_-1 >= 0, devs += ptime-global_points:(_-1):1); + 0.6*reduce(devs, _a+_, 0)/length(devs) + ); + impact = _norm_prob(path_point+point, ptime, dev); + //if(rtotal && impact < 0.000001*rtotal, throw()); // can work badly on segments with vastly diff lengths + components += [v, impact]; + rtotal += impact + ) + ); + try( + for(range(from_index, -1, -1), + [v,ptime,mode] = global_points:_; + dev = if (deviation > 0, deviation, + devs = []; + if (_+1 < length(global_points), devs += global_points:(_+1):1-ptime); + if (_-1 >= 0, devs += ptime-global_points:(_-1):1); + 0.6*reduce(devs, _a+_, 0)/length(devs) + ); + impact = _norm_prob(path_point+point, ptime, dev); + //if(ltotal && impact < 0.000001*ltotal, throw()); + components += [v, impact]; + ltotal += impact + ) + ); + total = rtotal+ltotal; + reduce(components, _a+_:0*(_:1/total), [0,0,0,0,0]) +); + +// Catmull-Rom spline +_interpolator_cr(from_index, point) -> +( + total = global_points:(from_index+1):1 - global_points:from_index:1; + p__1 = global_points:(if(from_index == 0, 0, from_index-1)):0; + p_0 = global_points:from_index:0; + p_1 = global_points:(from_index+1):0; + p_2 = global_points:(if(from_index == (length(global_points)-2), -1, from_index+2)):0; + r = point/total; // ratio within segment + (r*((2-r)*r-1) * p__1 + (r*r*(3*r-5)+2) * p_0 + r*((4 - 3*r)*r + 1) * p_1 + (r-1)*r*r * p_2) / 2 +); + +// store current path in a world file +save_as(file) -> +( + if (!global_points, exit('No path to save')); + path_nbt = nbt('{}'); + for (global_points, + point_nbt = nbt('{}'); + point_nbt:'duration' = _:1; + point_nbt:'type' = _:2; + for(_:0, put(point_nbt:'pos',str('%.6fd',_),_i)); // need to print to float string + //otherwise mojang will interpret 0.0d as 0i and fail to insert + put(path_nbt:'points', point_nbt, _i); + ); + write_file(file, 'nbt', path_nbt); + print('stored path as '+file); +); + +// loads path under the local file that +load(file) -> +( + path_nbt = read_file(file, 'nbt'); + if (!path_nbt, exit('No path to load: '+file)); + new_points = map(get(path_nbt, 'points[]'), [_:'pos[]', _:'duration', _:'type']); + if (!new_points || first(new_points, length(_:0) != 5), + exit('Incorrect data for :'+file); + ); + _start_with(_(outer(new_points)) -> new_points); + print('loaded '+file); +); + +// when closing - shut-down visualization and playback threads +__on_close() -> +( + global_playing_path = false; + global_showing_path = false; +); \ No newline at end of file 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 0000000..062f4b8 --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/chunk_display.sc @@ -0,0 +1,190 @@ +global_update_interval = 100; +global_chunk_colors = { + // loaded ticking + 3 -> [0xAAdd0000, 0x66990000], // lime, green + 2 -> [0xFF000000, 0x88000000], // red + 1 -> [0xFFcc0000, 0xbbaa0000], // yellowish + // 'unloaded' (in memory, so rather 'inaccessible') + 0 -> [0xcccccc00, 0x11111100], // gray / hi constast + // stable full generation (for '0's, all 1,2,3 are always 'full') + 'full' -> [0xbbbbbb00, 0x88888800], // gray + // unstable final bits + 'heightmaps' -> [0x33333300, 0x22222200], // checker + 'spawn' -> [0x00333300, 0x00222200], // checker + 'light_gen' -> [0x55550000, 0x44440000], // checker + // stable features + 'features' -> [0x88bb2200, 0x66882200], // green muddled + //stable terrain + 'liquid_carvers' -> [0x65432100, 0x54321000], // browns + //unstable terrain - not generated yet + 'carvers' -> [0x33003300, 0x22002200], // checker + 'surface' -> [0x33330000, 0x22220000], // checker + 'noise' -> [0x33000000, 0x22000000], // checker + 'biomes' -> [0x00330000, 0x00220000], // checker + 'structure_references' -> [0x00003300, 0x00002200], // checker + // stable + 'structure_starts' -> [0x66666600, 0x22222200], // darkgrey + // not stated yet + 'empty' -> [0xFF888800, 0xDD444400], // pink + // proper not in memory + null -> [0xFFFFFF00, 0xFFFFFF00], // hwite + + // ticket types + 'player' -> [0x0000DD00, 0x00008800], // blue + 'portal' -> [0xAA00AA00, 0xAA00AA00], // purple + 'dragon' -> [0xAA008800, 0xAA008800], // purple + 'forced' -> [0xAABBFF00, 0x8899AA00], // blue + 'light' -> [0xFFFF0000, 0xBBBB0000], // yellow + 'post_teleport' -> [0xAA00AA00, 0xAA00AA00], // purple + 'start' -> [0xDDFF0000, 0xDDFF0000], // lime, green + // recent chunk requests + 'unknown' -> [0xFF55FF00, 0xff99ff00] // pink purple +}; + +// map representing current displayed chunkloading setup +global_current_setup = null; + + + +__config() -> { + 'commands' -> { + ' ' -> ['__setup_tracker', null], + '
' -> '__setup_tracker', + 'clear' -> '__remove_previous_setup' + }, + 'arguments' -> { + 'radius' -> { + 'type' -> 'int', + 'suggest' -> [16], + 'min' -> 8, + 'max' -> 64 + }, + 'center' -> { + 'type' -> 'columnpos' + }, + } +}; + +// for legacy support +__on_player_uses_item(player, item_tuple, hand) -> +( + // which blocks in the inventory represent which dimension + global_block_markers = { + 'netherrack' -> 'the_nether', + 'grass_block' -> 'overworld', + 'end_stone' -> 'the_end' + }; + if (hand != 'mainhand', return()); + if (!has(global_block_markers, item_tuple:0), return()); + if (item_tuple:1%16 != 0, return()); + print('setting chunkloading via item does not work anymore, use /chunk_display instead'); +); + +__remove_previous_setup() -> +( + if (!global_current_setup, return()); + global_running = false; + global_current_setup = null; + global_status_cache = {}; +); + +__setup_tracker(dimension, radius, columnpos) -> +( + //player = player(); + if (global_current_setup, + __remove_previous_setup() + ); + + setup = {}; + setup:'plot_dimension' = current_dimension();// player ~ 'dimension'; + setup:'plot_center' = map(pos(player()), floor(_))-[0,1,0]; + setup:'radius' = radius; + setup:'source_dimension' = dimension; + 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' = if (columnpos, [columnpos:0, 0, columnpos:1], setup:'plot_center' * multiplier); + print(setup:'source_dimension'+' around '+setup:'source_center'+' with '+setup:'radius'+' radius ('+ (2*setup:'radius'+1)^2 +' chunks)'); + + [sx, sy, sz] = setup:'source_center'; + global_status_cache = {}; + loop( 2*setup:'radius'+1, dx = _ - setup:'radius'; + loop( 2*setup:'radius'+1, dz = _ - setup:'radius'; + source_pos = [sx+16*dx,sy,sz+16*dz]; + global_status_cache:source_pos = [null, 0]; + ) + ); + global_current_setup = setup; + global_running = true; + schedule(0, '__chunk_visualizer_tick'); +); + +global_status_cache = {}; + +__chunk_visualizer_tick() -> +( + setup = global_current_setup; + if(!setup, return()); + if (!global_running, return()); + player = player(); + show_activity = (player~'holds':0 == 'redstone_torch'); + yval = setup:'plot_center':1+16; + base_update = global_update_interval; + random_component = ceil(0.4*base_update); + duration_max = ceil(1.5*base_update); + + in_dimension( setup:'plot_dimension', + [sx, sy, sz] = setup:'source_center'; + [px, py, pz] = setup:'plot_center'; + source_center = setup:'source_center'; + radius = setup:'radius'; + source_dimension = setup:'source_dimension'; + + shapes = []; + now = tick_time(); + + loop( 2*radius+1, dx = _ - radius; + loop( 2*radius+1, dz = _ - radius; + changed = false; + source_pos = [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, + [type, level] = _; + if (show_activity || type != 'unknown', + status = type; + ); + ); + ); + [cached_status, expiry] = global_status_cache:source_pos; + changed = (status != cached_status); + if ( changed || (expiry < now), + global_status_cache:source_pos = [status, now+base_update+floor(rand(random_component))]; + bpos = [dx/2, yval, dz/2]; + bcol = global_chunk_colors:status:((dx+dz)%2); + shapes += ['box', duration_max, 'from', bpos, 'to', bpos + [0.5,0,0.5], + 'color', 0xffffff00, 'fill', bcol+128, 'follow', player, 'snap', 'xz']; + if (changed, + pbcol = global_chunk_colors:cached_status:((dx+dz)%2); + shapes += ['box', 0, 'from', bpos, 'to', bpos + [0.5,0,0.5], + 'color', 0xffffff00, 'fill', pbcol+128, 'follow', player, 'snap', 'xz']; + ); + ); + ); + ); + draw_shape(shapes); + ); + schedule(1, '__chunk_visualizer_tick') +) \ No newline at end of file diff --git a/src/main/resources/assets/carpet/scripts/distance_beta.sc b/src/main/resources/assets/carpet/scripts/distance_beta.sc new file mode 100644 index 0000000..c4ad94c --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/distance_beta.sc @@ -0,0 +1,154 @@ +import('math','_euclidean','_manhattan','_round'); + +global_display_modes = ['chat', 'line', 'centered_sphere', 'box']; + +__config()->{ + 'commands'->{ + 'from '->'set_start', + 'from to '-> _(start, end) -> (global_current_start = start; calculate(end)), + 'to '-> 'calculate', + 'clear' -> _() -> global_display_shapes = [], + 'clear last' -> _() -> if(global_display_shapes, delete(global_display_shapes, -1)), + 'display ' -> 'set_mode', + 'assist ' -> 'set_assist', + 'assist none' -> _() -> set_assist(null), + }, + 'arguments' -> { + 'start' -> {'type' -> 'location'}, + 'end' -> {'type' -> 'location'}, + 'mode' -> {'type' -> 'term', 'options' -> global_display_modes }, + 'assist' -> {'type' -> 'block', 'suggest' -> ['brown_carpet']}, + } +}; + +global_current_start = null; +global_display_shapes = []; +global_renderer_running = false; +global_current_mode = 'chat'; + +global_assist_block = null; + +set_assist(block) -> +( + + if (block == null, + global_assist_block = null; + handle_event('player_places_block', null); + handle_event('player_uses_item', null); + + , + global_assist_block = str(block); + handle_event('player_places_block', 'on_player_places_block'); + handle_event('player_uses_item', 'on_player_uses_item'); + ) +); + +global_generators = { + 'line' -> _(from, to) -> [ + ['line', 'from',from,'to',to] + ], + 'centered_sphere' -> _(from, to) -> [ + ['line', 'from',from,'to',to, 'line', 5, 'color', 0x88888888], + ['sphere', 'center',from,'radius',_euclidean(from,to)] + ], + 'box' -> _(from, to) -> [ + ['line', 'from',from,'to',to, 'line', 5, 'color', 0x88888888], + ['box', 'from',from,'to',to] + ], +}; + +_create_shapes(mode, from, to) -> +( + shapes = call(global_generators:mode, from+[0, 0.07, 0], to+[0, 0.07, 0]); + shapes += ['label', 'pos',to+[0,0.2,0], 'align', 'right', 'indent', -1.5, 'text',format('rb Cylindrical:')]; + shapes += ['label', 'pos',to+[0,0.2,0], 'align', 'left', 'text',_round(_euclidean(from, [to:0, from:1, to:2]), 0.001)]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 1, 'align', 'right', 'indent', -1.5, 'text',format('rb Manhattan:')]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 1, 'align', 'left', 'text',_round(_manhattan(from,to),0.001)]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 2, 'align', 'right', 'indent', -1.5, 'text',format('rb Euclidean:')]; + shapes += ['label', 'pos',to+[0,0.2,0], 'height', 2, 'align', 'left', 'text',_round(_euclidean(from,to),0.001)]; + map(shapes, put(_, 1, [40, 'player', player()], 'extend'); _); +); + +__mktp(pos) -> str('!/tp %.6f %.6f %.6f', pos); + +set_start(pos)->( + global_current_start = pos; + print(player(),format('gi Initial point set to: ', 'g '+pos, __mktp(pos))) +); + +set_mode(mode)->( + global_current_mode = mode; + print(player(),format('gi Set display mode to '+mode)) +); + +calculate(end)->( + if(global_current_start != null, + measure(end) + , // else + print(player(),format('r No initial point selected')); + ) +); + +on_player_places_block(player, item_tuple, hand, block) -> +( + if(block==global_assist_block, + effective_pos = pos(block)+[0.5, 0, 0.5]; + if(global_current_start==null||player~'sneaking',//wont complain for first carpet + set_start(effective_pos) + , // else + calculate(effective_pos) + ) + ) +); + +on_player_uses_item(player, item_tuple, hand) -> +( + if (item_tuple:0 == global_assist_block && hand == 'mainhand', + if (player ~'sneaking', + global_current_mode = global_display_modes:(global_display_modes ~ global_current_mode + 1); + display_title(player, 'actionbar', format('w Distance mode: ','e '+global_current_mode)); + , + if(global_display_shapes, delete(global_display_shapes, -1)) + ) + ) +); + +measure(end)-> +( + if (global_current_start == null, return()); + start = global_current_start; + if (global_current_mode == 'chat', + manhattan = _round(_manhattan(start, end),0.001);//rounding to nearest 0.01 for precision, but not ridiculous decimal places + spherical = _round(_euclidean(start, end), 0.001); + //useful for chunk distances etc. + cylindrical = _round(_euclidean(start, [end:0, start:1, end:2]), 0.001); + + print(player(),format( + 'w Distance between ', + 'c '+str('[%.1f, %.1f, %.1f]', start), __mktp(start), + 'w and ', + 'c '+str('[%.1f, %.1f, %.1f]', end), __mktp(end), 'w :\n', + 'w - Spherical: ', 'wb '+spherical+'\n', + 'w - Cylindrical: ', 'wb '+cylindrical+'\n', + 'w - Manhattan: ', 'wb '+manhattan + )); + , // else + global_display_shapes += _create_shapes(global_current_mode, start, end); + for (global_display_shapes, draw_shape(_) ); + if (!global_renderer_running, + global_renderer_running = true; + render() + ) + ) +); + +render()->( + if (!global_display_shapes, + global_renderer_running = false; + return(); + ); + for (global_display_shapes, + draw_shape(_); + ); + schedule(20,'render') +); diff --git a/src/main/resources/assets/carpet/scripts/draw_beta.sc b/src/main/resources/assets/carpet/scripts/draw_beta.sc new file mode 100644 index 0000000..6c64887 --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/draw_beta.sc @@ -0,0 +1,56 @@ +import('shapes','draw_sphere', 'draw_diamond', 'draw_filled_diamond', 'draw_pyramid', 'draw_prism'); //importing all the shape funcs from the other app + +__config() -> { + 'commands'->{ + 'sphere
'->_(c,r,b)->draw('draw_sphere',[c,r, true],b,null), + 'sphere
replace '->_(c,r,b,rp)->draw('draw_sphere',[c,r,true],b,rp), + 'ball
'->_(c,r,b)->draw('draw_sphere',[c,r,false],b,null), + 'ball
replace '->_(c,r,b,rp)->draw('draw_sphere',[c,r,false],b,rp), + 'diamond
hollow' -> _(c,r,b)->draw('draw_diamond',[c,r],b,null), + 'diamond
hollow replace '->_(c,r,b,rp)->draw('draw_diamond',[c, r], b, rp), + 'diamond
' -> _(c,r,b)->draw('draw_filled_diamond',[c,r],b,null), + 'diamond
replace '->_(c,r,b,rp)->draw('draw_filled_diamond',[c, r], b, rp), + 'pyramid
'->_(c,r,h,p,o,b,ho)->draw('draw_pyramid',[c,r,h,p,o,ho, true],b,null), + 'pyramid
replace '->_(c,r,h,p,o,b,ho,rp)->draw('draw_pyramid', [c,r,h,p,o,ho,true],b,rp), + 'cone
'->_(c,r,h,p,o,b,ho)->draw('draw_pyramid',[c,r,h,p,o,ho, false],b,null), + 'cone
replace '->_(c,r,h,p,o,b,ho,rp)->draw('draw_pyramid', [c,r,h,p,o,ho,false],b,rp), + 'cuboid
'->_(c,r,h,o,b,ho)->draw('draw_prism',[c,r,h,p,o,ho, true],b,null), + 'cuboid
replace '->_(c,r,h,o,b,ho,rp)->draw('draw_prism', [c,r,h,o,ho,true],b,rp), + 'cylinder
'->_(c,r,h,o,b,ho)->draw('draw_prism',[c,r,h,p,o,ho, false],b,null), + 'cylinder
replace '->_(c,r,h,o,b,ho,rp)->draw('draw_prism', [c,r,h,o,ho,false],b,rp), + }, + 'arguments'->{ + 'center'->{'type'->'pos', 'loaded'->'true'}, + 'radius'->{'type'->'int', 'suggest'->[], 'min'->0},//to avoid default suggestions + 'replacement'->{'type'->'blockpredicate'}, + 'height'->{'type'->'int', 'suggest'->[],'min'->0}, + 'orientation'->{'type'->'term', 'suggest'->['x','y','z']}, + 'pointing'->{'type'->'term','suggest'->['up','down']}, + 'hollow'->{'type'->'term','suggest'->['hollow','solid']}, + }, + 'scope'->'global' +}; + +_block_matches(existing, block_predicate) -> +( + [name, block_tag, properties, nbt] = block_predicate; + + (name == null || name == existing) && + (block_tag == null || block_tags(existing, block_tag)) && + all(properties, block_state(existing, _) == properties:_) && + (!tag || tag_matches(block_data(existing), tag)) +); + +draw(what, args, block, replacement)->(//custom setter cos it's easier + positions = call(what,args); //returning blocks to be set + + affected = 0; + + for(positions, + existing = block(_); + if(block != existing && (!replacement || _block_matches(existing, replacement)), + affected += bool(set(existing,block)) + ) + ); + print(format('gi Filled ' + affected + ' blocks')); +); diff --git a/src/main/resources/assets/carpet/scripts/event_test.sc b/src/main/resources/assets/carpet/scripts/event_test.sc new file mode 100644 index 0000000..a86c18d --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/event_test.sc @@ -0,0 +1,392 @@ + +__on_player_jumps(player) -> ( + print(''); + print('__on_player_jumps(player)'); + print('Player '+player+' jumps.') +); + +__on_player_deploys_elytra(player) -> ( + print(''); + print('__on_player_deploys_elytra(player)'); + print('Player '+player+' deploys elytra.') +); + +__on_player_wakes_up(player) -> ( + print(''); + print('__on_player_wakes_up(player)'); + print('Player '+player+' wakes up') +); + +__on_player_escapes_sleep(player) -> ( + print(''); + print('__on_player_escapes_sleep(player)'); + print('Player '+player+' wakes up using ESC key') +); + +__on_player_rides(player, forward, strafe, jumping, sneaking) -> +( + print(''); + print('__on_player_rides(player, forward, strafe, jumping, sneaking)'); + print('player rides a vehicle:'); + print(' - player: '+player); + print(' - forward/backward: '+forward); + print(' - strafing: '+strafe); + print(' - jumping: '+jumping); + print(' - sneaking: '+sneaking) +); + +__on_player_uses_item(player, item_tuple, hand) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_uses_item(player, item_tuple, hand)'); + print('player uses an item:'); + print(' - player: '+player); + print(' - item:'); + print(' > name: '+item); + print(' > count: '+count); + print(' > nbt: '+nbt); + print(' - hand: '+hand) +); + +__on_player_clicks_block(player, block, face) -> +( + print(''); + print('__on_player_clicks_block(player, block, face)'); + print('player clicks a block:'); + print(' - player: '+player); + print(' - block: '+block+' at '+map(pos(block), str('%.2f',_))); + print(' - face: '+face) +); + + +__on_player_right_clicks_block(player, item_tuple, hand, block, face, hitvec) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_right_clicks_block(player, item_tuple, hand, block, face, hitvec) '); + print('block right clicked by player:'); + print(' - player: '+player); + print(' - item: '+item); + print(' - hand: '+hand); + print(' - block: '+block+' at '+map(pos(block), str('%.2f',_))); + print(' - face: '+face); + print(' - hitvec: '+map(hitvec, str('%.2f',_))) +); + +__on_player_breaks_block(player, block) -> +( + print(''); + print('__on_player_breaks_block(player, block)'); + print('player breaks block:'); + print(' - player: '+player); + print(' - block: '+block+' at '+map(pos(block), str('%.2f',_))) +); + +__on_player_interacts_with_entity(player, entity, hand) -> +( + print(''); + print('__on_player_interacts_with_entity(player, entity, hand)'); + print('player interacts with entity:'); + print(' - player: '+player); + print(' - entity: '+entity+' at '+map(pos(entity), str('%.2f',_))); + print(' - hand: '+hand) +); + +__on_player_attacks_entity(player, entity) -> +( + print(''); + print('__on_player_attacks_entity(player, entity)'); + print('player attacks entity:'); + print(' - player: '+player); + print(' - entity: '+entity+' at '+map(pos(entity), str('%.2f',_))) +); + +__on_player_starts_sneaking(player) -> ( + print(''); + print('__on_player_starts_sneaking(player)'); + print('Player '+player+' starts sneaking.') +); + +__on_player_stops_sneaking(player) -> ( + print(''); + print('__on_player_stops_sneaking(player)'); + print(' Player '+player+' stops sneaking.') +); + +__on_player_starts_sprinting(player) -> ( + print(''); + print('__on_player_starts_sprinting(player)'); + print(' Player '+player+' starts sprinting.') +); + +__on_player_stops_sprinting(player) -> ( + print(''); + print('__on_player_stops_sprinting(player)'); + print('Player '+player+' stops sprinting.') +); + +__on_player_releases_item(player, item_tuple, hand) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_releases_item(player, item_tuple, hand)'); + print('player releases an item:'); + print(' - player: '+player); + print(' - item:'); + print(' > name: '+item); + print(' > count: '+count); + print(' > nbt: '+nbt); + print(' - hand: '+hand) +); + +__on_player_finishes_using_item(player, item_tuple, hand) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_finishes_using_item(player, item_tuple, hand)'); + print('player finishes using an item:'); + print(' - player: '+player); + print(' - item:'); + print(' > name: '+item); + print(' > count: '+count); + print(' > nbt: '+nbt); + print(' - hand: '+hand) +); + +__on_player_drops_item(player) -> ( + print(''); + print('__on_player_drops_item(player)'); + print('Player '+player+' drops current item.') +); + +__on_player_drops_stack(player) -> ( + print(''); + print('__on_player_drops_stack(player)'); + print('Player '+player+' drops current stack.') +); + +__on_player_interacts_with_block(player, hand, block, face, hitvec) -> +( + print(''); + print('__on_player_interacts_with_block(player, hand, block, face, hitvec) '); + print('block right clicked by player:'); + print(' - player: '+player); + print(' - hand: '+hand); + print(' - block: '+block+' at '+map(pos(block), str('%.2f',_))); + print(' - face: '+face); + print(' - hitvec: '+map(hitvec, str('%.2f',_))) +); +__on_player_placing_block(player, item_tuple, hand, block) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_placing_block(player, item_tuple, hand, block)'); + print('player about to place a block:'); + print(' - player: '+player); + print(' - block: '+block+' at '+map(pos(block), str('%.2f',_))); + print(' - hand: '+hand); + print(' - item:'); + print(' > name: '+item); + print(' > count: '+count); + print(' > nbt: '+nbt); +); +__on_player_places_block(player, item_tuple, hand, block) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_places_block(player, item_tuple, hand, block)'); + print('player places a block:'); + print(' - player: '+player); + print(' - block: '+block+' at '+map(pos(block), str('%.2f',_))); + print(' - hand: '+hand); + print(' - item:'); + print(' > name: '+item); + 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) +); + +__on_player_message(player, message) -> +( + print(''); + print('__on_player_message(player, message)'); + print('Player '+player+' sent message: '+message) +); + +__on_player_command(player, command) -> +( + print(''); + print('__on_player_command(player, command)'); + print('Player '+player+' sent command: '+command) +); + +__on_player_chooses_recipe(player, recipe, full_stack) -> +( + print(''); + print('__on_player_chooses_recipe(player, recipe, full_stack)'); + print('player chooses recipe:'); + print(' - player: '+player); + print(' - recipe: '+recipe); + print(' - full stack: '+ full_stack); +); + +__on_player_switches_slot(player, from, to) -> +( + print(''); + print('__on_player_switches_slot(player, from, to)'); + print('player switches inventory slot:'); + print(' - player: '+player); + print(' - from: '+ from); + print(' - to: '+ to); +); + +__on_player_swaps_hands(player) -> ( + print(''); + print('__on_player_swaps_hands(player)'); + print(' Player '+player+' swaps hands.') +); + +// too much spam +//__on_player_collides_with_entity(player, entity) -> +//( +// print(''); +// print('__on_player_collides_with_entity(player, entity)'); +// print(' Player '+player+' collides with '+entity); +//); + +__on_player_picks_up_item(player, item_tuple) -> +( + l(item, count, nbt) = item_tuple || l('None', 0, null); + print(''); + print('__on_player_picks_up_item(player, item)'); + print(' - player: '+player); + print(' - item:'); + print(' > name: '+item); + print(' > count: '+count); + print(' > nbt: '+nbt); +); + +__on_player_trades(player, entity, buy_left, buy_right, sell) -> +( + print(''); + print('__on_player_trades(player, item)'); + print('player trades:'); + print(' - player: '+player); + print(' - entity: '+ entity); + print(' - buy_left: '+ buy_left); + print(' - buy_right: '+ buy_right); + print(' - sell: '+ sell) +); + +__on_player_swings_hand(player, hand) -> +( + print(''); + print('__on_player_swings_hand(player, hand)'); + print('player swings hand:'); + print(' - player: '+player); + print(' - hand: '+hand); +); + +__on_start() -> +( + print(''); + print('__on_start(player)'); + print('App is starting for '+player()); + logger('starting for '+player()); +); + +__on_close() -> +( + print(''); + print('__on_close(player)'); + print('App is closing for '+player()); + logger('App is closing for '+player()); +); + +__on_explosion(pos, power, source, causer, mode, fire) -> +( + print(''); + print('__on_explosion(pos, power, source, causer, mode, fire)'); + print('explosion:'); + print(' - pos: '+pos); + print(' - power: '+power); + print(' - source: '+source); + print(' - causer: '+causer); + print(' - mode: '+mode); + print(' - fire: '+fire); + +); + +__on_explosion_outcome(pos, power, source, causer, mode, fire, blocks, entities) -> +( + print(''); + print('__on_explosion_outcome(pos, power, source, causer, mode, fire, blocks, entities)'); + print('explosion outcome:'); + print(' - pos: '+pos); + print(' - power: '+power); + print(' - source: '+source); + print(' - causer: '+causer); + print(' - mode: '+mode); + print(' - fire: '+fire); + print(' - number or affected blocks: '+length(blocks)); + print(' - number of affected entities: '+length(entities)); +); diff --git a/src/main/resources/assets/carpet/scripts/math.scl b/src/main/resources/assets/carpet/scripts/math.scl new file mode 100644 index 0000000..4af9ae3 --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/math.scl @@ -0,0 +1,5 @@ +_euclidean_sq(vec1, vec2) -> reduce(vec1 - vec2, _a + _*_, 0); +_euclidean(vec1, vec2) -> sqrt(reduce(vec1 - vec2, _a + _*_, 0)); +_manhattan(vec1, vec2) -> reduce(vec1-vec2,_a+abs(_),0); +_vec_length(vec) -> sqrt(reduce(vec, _a + _*_, 0)); +_round(num,precision) -> round(num/precision)*precision; diff --git a/src/main/resources/assets/carpet/scripts/overlay.sc b/src/main/resources/assets/carpet/scripts/overlay.sc new file mode 100644 index 0000000..f693304 --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/overlay.sc @@ -0,0 +1,315 @@ +import('math', '_euclidean_sq', '_euclidean'); + +global_renderers = { + 'structures' -> { + 'active' -> false, + 'handler' -> '__structure_renderer', + 'tasks' -> {}, + 'range' -> 3, + 'max_pieces' -> 6 + }, + 'chunks' -> { + 'active' -> false, + 'handler' -> '__chunk_renderer', + 'tasks' -> {}, + 'range' -> 6 + }, + 'shapes' -> { + 'active' -> false, + 'handler' -> '__shape_renderer', + 'tasks' -> {}, + 'range' -> 8 + }, + 'portals' -> { + 'active' -> false, + 'handler' -> '__portal_renderer', + 'tasks' -> {} + } +}; + +global_shapes = {'sphere' -> [], 'box' -> []}; + +__config() -> +{ + 'commands' -> { + 'structure ' -> _(s) -> __toggle(s, 'structures'), + 'slime_chunks' -> ['__toggle', 'slime_chunks', 'chunks'], + 'portal coordinates' -> ['__toggle', 'coords', 'portals'], + 'portal links' -> ['__toggle', 'links', 'portals'], + ' following ' -> ['display_shape', [null,0xffffffff], true], + ' at ' -> ['display_shape', [null,0xffffffff], false], + ' following ' -> ['display_shape', true], + ' at ' -> ['display_shape', false], + ' clear' -> 'clear_shape', + 'clear' -> 'clear', + }, + 'arguments' -> { + 'structure' -> {'type' -> 'identifier', 'suggest' -> plop():'structures' }, + 'radius' -> {'type' -> 'int', 'min' -> 0, 'max' -> 1024, 'suggest' -> [128, 24, 32]}, + 'shape' -> {'type' -> 'term', 'options' -> keys(global_shapes) }, + 'color' -> {'type' -> 'teamcolor'} + } +}; + + +display_shape(shape, r, entities, color, following) -> +( + thicc = max(1, floor(6-length(entities)/2)); + fillc = 250 - 100/(length(entities)+1); + for (entities, + if (shape == 'sphere', + shape_config = { + 'center' -> if(following, [0,0,0], pos(_)), + 'radius' -> r + } + , shape == 'box', + shape_config = { + 'from' -> if(following, [-r,-r,-r], pos(_)-r), + 'to' -> if(following, [r,r,r], pos(_)+r) + } + ); + if (shape_config, + if (following, shape_config:'follow' = _); + shape_config:'fill' = color:1 - fillc; + shape_config:'color' = color:1; + shape_config:'line' = thicc; + draw_shape(shape, 72000, shape_config); + global_shapes:shape += shape_config; + ) + ) +); + + +clear_shape(shape) -> +( + for(global_shapes:shape, draw_shape(shape, 0, _)); + global_shapes:shape = []; +); + +clear() -> +( + for(global_renderers, global_renderers:_:'tasks' = {} ); + for(global_shapes, clear_shape(_)); +); + +__toggle(feature, renderer) -> +( + p = player(); + config = global_renderers:renderer; + if( has(config:'tasks':feature), + delete(config:'tasks':feature); + print(p, format('gi disabled '+feature+' overlays')); + , + config:'tasks' += feature; + if (!config:'active', call(config:'handler', p~'name')); + print(p, format('gi enabled '+feature+' overlays')); + ) +); + +__should_run(renderer, player_name) -> +( + config = global_renderers:renderer; + if (length(config:'tasks')==0, config:'active' = false; return([null, null])); + p = player(player_name); + if (!p, config:'active' = false; clear(); return([null, null])); + config:'active' = true; + [p, config]; +); + +__structure_renderer(player_name) -> +( + [p, config] = __should_run('structures', player_name); + if (!p, return()); + in_dimension(p, + ppos = pos(p); + starts = {}; + r = config:'range'; + for(range(-r,r), cx =_; + for (range(-r,r), cz = _; + ref_pos = ppos + [16*cx,0,16*cz]; + for(filter(structure_references(ref_pos), has(config:'tasks':_)), + name = _; + for(structure_references(ref_pos, name), + starts += [_, name]; + ) + ) + ) + ); + for (starts, [position, name] = _; + structure_data = structures(position):name; + if (!structure_data, continue()); // messed up references - shouldn't happen + 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); + draw_shape('box', 15, 'from', from, 'to', to+1, 'color', 0x00FFFFFF, 'line', 3, 'fill', 0x00FFFF12); + structure_pieces = slice(sort_key(structure_pieces, _euclidean_sq((_:2+_:3)/2,ppos)), 0, config:'max_pieces'); + for (structure_pieces, [piece, direction, from, to] = _; + 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(10, '__structure_renderer', player_name); +); + +__chunk_renderer(player_name) -> +( + [p, config] = __should_run('chunks', player_name); + if (!p, return()); + in_dimension( p, + // get lower corner of the chunk + ppos = map(pos(p)/16, floor(_))*16; + ppos:1 = 0; + rang = config:'range'; + for(range(-rang,rang), cx =_; + for (range(-rang,rang), cz = _; + ref_pos = ppos + [16*cx,0,16*cz]; + if(has(config:'tasks':'slime_chunks') && in_slime_chunk(ref_pos), + player_distance = _euclidean(ppos, ref_pos); + top_00 = ref_pos + [0, top('terrain', ref_pos)+10, 0]; + top_11 = ref_pos + [16, top('terrain', ref_pos+l(15,0,15))+10, 16]; + top_10 = ref_pos + [16, top('terrain', ref_pos+l(15, 0, 0))+10, 0]; + top_01 = ref_pos + [0, top('terrain', ref_pos+l(0, 0, 15))+10, 16]; + r = 30; + g = 220; + b = 30; + a = max(0, 255-player_distance); + color = a+256*(b+256*(g+256*r)); + draw_shape([ + ['line', 15, 'from', top_00, 'to', top_10, 'color', color, 'line', 3], + ['line', 15, 'from', top_10, 'to', top_11, 'color', color, 'line', 3], + ['line', 15, 'from', top_11, 'to', top_01, 'color', color, 'line', 3], + ['line', 15, 'from', top_01, 'to', top_00, 'color', color, 'line', 3], + ['line', 15, 'from', top_00, 'to', top_11, 'color', color, 'line', 3], + ['line', 15, 'from', top_01, 'to', top_10, 'color', color, 'line', 3] + ]); + ) + ) + ) + ); + schedule(10, '__chunk_renderer', player_name); +); + +__portal_renderer(player_name) -> +( + [p, config] = __should_run('portals', player_name); + if (!p, return()); + dim = p~'dimension'; + shapes = []; + shape_duration = 6; + ppos = pos(p); + py = ppos:1; + if (dim == 'overworld', + // + nether_pos = [ppos:0/8, py, ppos:2/8]; + if (has(config:'tasks':'coords'), + ow_x = 8*floor(nether_pos:0); + ow_z = 8*floor(nether_pos:2); + for (range(-5,6), dx = _; + for (range(-5,6), dz = _; + x = ow_x + 8*dx; + z = ow_z + 8*dz; + nx = x / 8; + nz = z / 8; + shapes += ['line', shape_duration, 'from', [x,0,z], 'to', [x,255,z], 'line', 2]; + shapes += ['box', shape_duration, 'from', [x,0,z], 'to', [x+8,255,z+8], 'color', 0x00000000, 'fill', 0xffffff05]; + shapes += ['label', shape_duration, 'pos', [x+4, 1,z+4], 'text', [nx, floor(py), nz], 'follow', p, 'snap', 'y']; + ) + ); + ); + if (has(config:'tasks':'links'), + in_dimension('the_nether', + portals = map(poi(nether_pos, 16, 'nether_portal', 'any', true), [x,y,z]=_:2; [[x,y,z], [8*x,y,8*z]]); + if (portals, + sorted_portals = __sort_portals(nether_pos, portals); + offset = p~'look' + [0,p~'eye_height',0]; + for(sorted_portals, + [pos, display] = _; + color = if(_i, 0x00999999, 0x990099ff); + to_rel = display-ppos; + shapes += ['line', shape_duration, 'line', 5, 'from', [0,0.5,0], 'to', to_rel, 'color', color,'follow', p]; + + to_dist = _euclidean([0,0,0],to_rel); + if (to_dist > 3, + direction = to_rel / to_dist; + shapes += ['label', shape_duration, 'pos', [0,0.5,0]+(2+1*_i)*direction, 'text', pos, 'color', color, 'follow', p]; + ) + + ) + ) + ) + ); + , dim == 'the_nether', + look = query(p, 'trace', 10, 'blocks', 'exact'); + if (look, + ly = look:1; + ow_pos = [8*look:0, look:1, 8*look:2]; + nx = floor(8*look:0)/8; + nz = floor(8*look:2)/8; + shapes += ['box', shape_duration, 'from', [nx, ly-1, nz], 'to', [nx+0.125, ly+1, nz+0.125]]; + if (has(config:'tasks':'coords'), + shapes += ['label', shape_duration, 'pos', look-ppos+[0,0.25,0], 'follow', p, 'text', map(ow_pos,floor(_))]; + ); + + if (has(config:'tasks':'links'), + in_dimension('overworld', + portals = map(poi(ow_pos, 128, 'nether_portal', 'any', true), [x,y,z]=_:2; [[x,y,z], [x/8,y,z/8]]); + if (portals, + sorted_portals = __sort_portals(ow_pos, portals); + for(sorted_portals, + [pos, display] = _; + color = if(_i, 0x00999999, 0x990099ff); + to_rel = display-ppos; + shapes += ['line', shape_duration, 'line', 5, 'from', look-ppos+[0,1.0,0], 'to', to_rel, 'color', color, 'follow', p]; + + to_dist = _euclidean([0,0,0],to_rel); + if (to_dist > 3, + direction = to_rel / to_dist; + shapes += ['label', shape_duration, 'pos', look-ppos+[0,1.1,0]+(0.2+0.5*_i)*direction, 'text', pos, 'color', color, 'follow', p] + ); + + ) + ) + ) + ) + ) + ); + if (shapes, in_dimension(p, draw_shape(shapes))); + schedule(5, '__portal_renderer', player_name); +); + +__sort_portals(ref_pos, portals) -> +( + point_map = {}; + for (portals, point_map:(_:0) = _:1); + sorted_portals = sort_key(keys(point_map), _euclidean_sq(ref_pos, _)+0.0001*(_:1)); + output_locations = []; + seen_portals = {}; + for (sorted_portals, + [x,y,z] = _; + if ( !has(seen_portals, [x,y+1,z]) && !has(seen_portals, [x,y-1,z]), + output_locations += _; + ); + seen_portals += _; + ); + base_portals = []; + seen_bases = {}; + for (output_locations, + [x,y,z] = _; + while(has(seen_portals, [x,y,z]), 256, y += -1); + y += 1; + seen_bases += [x,y,z]; + if (!has(seen_bases, [x-1,y,z]) && !has(seen_bases, [x+1,y,z]) + && !has(seen_bases, [x,y,z-1]) && !has(seen_bases, [x,y,z+1]), + base_portals += [x,y,z]; + ) + ); + map(base_portals, [_, point_map:_]); +); diff --git a/src/main/resources/assets/carpet/scripts/shapes.scl b/src/main/resources/assets/carpet/scripts/shapes.scl new file mode 100644 index 0000000..e15a065 --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/shapes.scl @@ -0,0 +1,233 @@ +//This is a library app, mostly for the draw command, which allows to import these shape-drawing functions into your own app. +//Arguments for each are to be given as a singleton list, for ease of use. This is with the exception of the fill_flat functions, with +//the assumption that they're unlikely to be used outside of internal calls, but it is still available to be called if necessary. +//Make sure you read the arguments and input them in the correct order, or it will mess up. +//these functions return a set of all the positions in the shape, given the parameters, without repeating a position. +//This way, you can efficiently iterate over the positions once having generated them. See draw_beta.sc for an example. todo change to draw once renamed + + +draw_sphere(args)->( + [centre, radius, hollow] = args; + positions = {}; + [cx,cy,cz]=centre; + for(range(-90, 90, 45/radius), + cpitch = cos(_); + spitch = sin(_); + for(range(0, 180, 45/radius), + cyaw = cos(_)*cpitch*radius; + syaw = sin(_)*cpitch*radius; + if(hollow, + positions += [cx+cyaw,cy+spitch*radius,cz+syaw]; + positions += [cx+cos(_+180)*cpitch*radius,cy+spitch*radius,cz+sin(_+180)*cpitch*radius], + for(range(-syaw,syaw+1), + positions += [cx+cyaw*cpitch,cy+spitch*radius,cz+_] + ) + ) + ) + ); + positions +); + +draw_diamond(args)->( + [pos, radius] = args; + positions = {}; + c_for(r=0, r( + [pos, radius] = args; + positions = {}; + for(diamond(pos,radius,radius), + positions += pos(_) + ); + positions +); + +draw_pyramid(args)->( + [pos, radius, height, pointing, orientation, fill_type, is_square] = args;//todo change to hollow boolean arg + positions = {}; + hollow = fill_type=='hollow'; + pointup = pointing=='up'; + for(range(height), + r = if(pointup, radius * ( 1- _ / height) -1, radius * _ / height); + positions += fill_flat(pos, _, r, is_square, orientation, !((pointup&&_==0)||(!pointup && _==height-1)) && hollow)//Always close bottom off + ); + positions +); + +draw_prism(args)->( + [pos, rad, height, orientation, fill_type, is_square]=args;//todo change to hollow boolean arg + positions = {}; + hollow = fill_type =='hollow'; + radius = rad+0.5; + filled_circle_points = fill_flat(pos, 0, radius, is_square, orientation, false); //I dont actually need to generate all the points lol + offset_vector = if(orientation=='x', [1,0,0], orientation=='y', [0,1,0],[0,0,1]); + if(hollow, + hollow_circle_points = fill_flat(pos, 0, radius, is_square, orientation, true); + + for(filled_circle_points,//Always close ends off + positions+=_; + positions+=_+offset_vector*(height-1) + ); + + for(range(1, height-1),//cos Im adding ends as filled anyways + offset=_; + for(hollow_circle_points, + positions+= _ + offset_vector * offset + ) + ), + + for(range(height), + offset = _; + for(filled_circle_points, + positions += _ + offset_vector * offset + ) + ); + ); + positions +); + +fill_flat(pos, offset, dr, rectangle, orientation, hollow)-> + if(rectangle, + fill_flat_rectangle(pos, offset, dr, orientation, hollow), + fill_flat_circle(pos, offset, dr, orientation, hollow) + ); + +fill_flat_circle(pos, offset, dr, orientation, hollow) ->( //Bresenham circle algorithm to be super efficient + r = floor(dr); + a=0; + b=r; + d=3-(2*r); + + positions = {}; + + positions += fill_flat_circle_points(pos, orientation, offset, hollow, a, b); + + while(a<=b, 10^10, + if(d<=0, + d += (4*a)+6, + d += (4*a)-(4*b)+10; + b=b-1 + ); + a+=1; + positions += fill_flat_circle_points(pos, orientation, offset, hollow, a, b) + ); + positions +); + +fill_flat_circle_points(pos, orientation, offset, hollow, a, b) ->( + [x, y, z] = pos; + positions = {}; + if(orientation == 'x', + if(hollow, + positions += [x + offset, a+y, b+z]; + positions += [x + offset, a+y,-b+z]; + positions += [x + offset,-a+y,-b+z]; + positions += [x + offset,-a+y, b+z]; + positions += [x + offset, b+y, a+z]; + positions += [x + offset, b+y,-a+z]; + positions += [x + offset,-b+y,-a+z]; + positions += [x + offset,-b+y, a+z], + c_for(c = -a, c<=a, c+=1, + positions += [x + offset,c+y, b + z]; + positions += [x + offset,c+y,-b + z] + ); + c_for(c = -b, c<=b, c+=1, + positions += [x + offset, a+y,c + z]; + positions += [x + offset,-a+y,c + z] + ) + ), + orientation=='y', + if(hollow, + positions += [ a+x, y + offset, b+z]; + positions += [ a+x, y + offset,-b+z]; + positions += [-a+x, y + offset,-b+z]; + positions += [-a+x, y + offset, b+z]; + positions += [ b+x, y + offset, a+z]; + positions += [ b+x, y + offset,-a+z]; + positions += [-b+x, y + offset,-a+z]; + positions += [-b+x, y + offset, a+z], + c_for(c = -a, c<=a, c+=1, + positions += [c+x,y + offset, b + z]; + positions += [c+x,y + offset,-b + z] + ); + c_for(c = -b, c<=b, c+=1, + positions += [ a+x,y + offset,c + z]; + positions += [-a+x,y + offset,c + z] + ) + ), + orientation=='z', + if(hollow, + positions += [ a+x, b+y, z + offset]; + positions += [ a+x,-b+y, z + offset]; + positions += [-a+x,-b+y, z + offset]; + positions += [-a+x, b+y, z + offset]; + positions += [ b+x, a+y, z + offset]; + positions += [ b+x,-a+y, z + offset]; + positions += [-b+x,-a+y, z + offset]; + positions += [-b+x, a+y, z + offset], + c_for(c = -a, c<=a, c+=1, + positions += [c+x, b + y,z + offset]; + positions += [c+x,-b + y,z + offset] + ); + c_for(c = -b, c<=b, c+=1, + positions += [ a+x,c + y,z + offset]; + positions += [-a+x,c + y,z + offset] + ) + ) + ) +); + +fill_flat_rectangle(pos, offset, dr, orientation, hollow) -> ( + r = floor(dr); + drsq = dr^2; + positions = {}; + if(orientation == 'x', + c_for(a = -r, a <=r, a +=1, + if(hollow, + positions += [r + offset, a, r]; + positions += [r + offset, a,-r]; + positions += [r + offset, r, a]; + positions += [r + offset,-r, a], + c_for(b = -r, b <=r, b +=1, + positions += [r + offset, a, b] + ) + ) + ), + orientation == 'y', + c_for(a = -r, a <=r, a +=1, + if(hollow, + positions += [ r, r + offset,a]; + positions += [-r, r + offset,a]; + positions += [a, r + offset, r]; + positions += [a, r + offset,-r], + c_for(b = -r, b <=r, b +=1, + positions += [a, r + offset, b] + ) + ) + ), + orientation == 'z', + c_for(a = -r, a <=r, a +=1, + if(hollow, + positions += [ r, a,r + offset]; + positions += [-r, a,r + offset]; + positions += [a, r, r + offset]; + positions += [a,-r, r + offset], + c_for(b = -r, b <=r, b +=1, + positions += [b, a, r + offset] + ) + ) + ), + ); + positions +); diff --git a/src/main/resources/assets/carpet/scripts/stats_test.sc b/src/main/resources/assets/carpet/scripts/stats_test.sc new file mode 100644 index 0000000..076e8ea --- /dev/null +++ b/src/main/resources/assets/carpet/scripts/stats_test.sc @@ -0,0 +1,5 @@ +__on_statistic(player, category, event, value) -> +( + print('__on_statistic(player, category, event, value) -> '); + print(' ['+join(', ',l(player, category, event, value))+']'); +); \ No newline at end of file diff --git a/src/main/resources/carpet.accesswidener b/src/main/resources/carpet.accesswidener new file mode 100644 index 0000000..8aa2daf --- /dev/null +++ b/src/main/resources/carpet.accesswidener @@ -0,0 +1,20 @@ +accessWidener v1 named + +accessible class net/minecraft/server/level/ChunkMap$DistanceManager +accessible class net/minecraft/server/level/ThreadedLevelLightEngine$TaskType +accessible class net/minecraft/world/item/crafting/Ingredient$Value +accessible class net/minecraft/world/level/lighting/BlockLightSectionStorage$BlockDataLayerStorageMap +accessible class net/minecraft/world/level/border/WorldBorder$BorderExtent +accessible class net/minecraft/world/level/border/WorldBorder$StaticBorderExtent +accessible class net/minecraft/server/MinecraftServer$ReloadableResources +accessible class net/minecraft/world/level/biome/Biome$ClimateSettings +accessible class net/minecraft/world/level/block/entity/SculkSensorBlockEntity$VibrationUser + +#TODO fields and methods should be fetched via interfaces unless there is a good reason not to +accessible method net/minecraft/world/level/border/WorldBorder getListeners ()Ljava/util/List; + +accessible method net/minecraft/server/level/DistanceManager purgeStaleTickets ()V +#cause I don't want to deal with the interfaces until stuff is stable +accessible method net/minecraft/world/level/block/entity/SkullBlockEntity fetchGameProfile (Ljava/lang/String;)Ljava/util/concurrent/CompletableFuture; + +accessible field net/minecraft/world/level/block/state/BlockBehaviour UPDATE_SHAPE_ORDER [Lnet/minecraft/core/Direction; diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json new file mode 100644 index 0000000..6b68dcf --- /dev/null +++ b/src/main/resources/carpet.mixins.json @@ -0,0 +1,208 @@ +{ + "required": true, + "package": "carpet.mixins", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "BarrierBlock_updateSuppressionBlockMixin", + "SpawnState_scarpetMixin", + "MobCategory_spawnMixin", + "Commands_customCommandsMixin", + "ServerGamePacketListenerImplMixin", + + "PerfCommand_permissionMixin", + "FillCommandMixin", + "CloneCommands_fillUpdatesMixin", + "SetBlockCommand_fillUpdatesMixin", + "ForceLoadCommand_forceLoadLimitMixin", + "BlockInput_fillUpdatesMixin", + "Level_fillUpdatesMixin", + "LevelChunk_fillUpdatesMixin", + "StructureBlockEntity_fillUpdatesMixin", + "StructureBlockEntity_limitsMixin", + "ServerboundSetStructureBlockPacketMixin", + "StructureTemplate_fillUpdatesMixin", + "ServerGamePacketListenerImpl_interactionUpdatesMixin", + "FlowingFluid_liquidDamageDisabledMixin", + "ServerStatus_motdMixin", + "MinecraftServer_pingPlayerSampleLimit", + "HugeFungusFeatureMixin", + "PiglinBrute_getPlacementTypeMixin", + "MinecraftServer_coreMixin", + + "MinecraftServer_tickspeedMixin", + "Level_tickMixin", + "ServerLevel_tickMixin", + "BoundTickingBlockEntity_profilerMixin", + "ServerChunkCache_profilerMixin", + "ChunkMap_profilerMixin", + "ServerFunctionManager_profilerMixin", + "WorldBorder_syncedWorldBorderMixin", + "ChunkMap_scarpetChunkCreationMixin", + "LevelEntityGetterAdapter_scarpetMixin", + "ChunkHolder_scarpetChunkCreationMixin", + "ThreadedLevelLightEngine_scarpetMixin", + "DynamicGraphMinFixedPoint_resetChunkInterface", + "DistanceManager_scarpetChunkCreationMixin", + "LevelLightEngine_scarpetChunkCreationMixin", + "LayerLightEngine_scarpetChunkCreationMixin", + "LayerLightSectionStorage_scarpetChunkCreationMixin", + "DistanceManager_spawnChunksMixin", + "ThreadedLevelLightEngine_scarpetChunkCreationMixin", + "LivingEntity_cleanLogsMixin", + "MobMixin", + "Villager_aiMixin", + "Player_fakePlayersMixin", + "ServerGamePacketListenerImpl_coreMixin", + "MinecraftServer_scarpetMixin", + "ExperienceOrb_xpNoCooldownMixin", + "Player_xpNoCooldownMixin", + "ServerPlayer_actionPackMixin", + "PlayerList_coreMixin", + "PlayerList_fakePlayersMixin", + "RedstoneWireBlock_fastMixin", + "WoolCarpetBlock_placeMixin", + "SummonCommand_lightningMixin", + "PistonStructureResolver_pushLimitMixin", + "PoweredRailBlock_powerLimitMixin", + "LivingEntity_maxCollisionsMixin", + "Level_getOtherEntitiesLimited", + "CoralPlantBlock_renewableCoralMixin", + "CoralFanBlock_renewableCoralMixin", + "CoralFeature_renewableCoralMixin", + "BuddingAmethystBlock_movableAmethystMixin", + "LiquidBlock_renewableBlackstoneMixin", + "LiquidBlock_renewableDeepslateMixin", + "LavaFluid_renewableDeepslateMixin", + "Level_scarpetPlopMixin", + "WorldGenRegion_scarpetPlopMixin", + "StructurePiece_scarpetPlopMixin", + "PieceGeneratorSupplier_plopMixin", + "ServerGamePacketListenerImpl_scarpetEventsMixin", + "ServerPlayerGameMode_scarpetEventsMixin", + "Player_parrotMixin", + "SaplingBlock_desertShrubsMixin", + "EntityMixin", + "Guardian_renewableSpongesMixin", + "Husk_templesMixin", + "ChunkGenerator_customMobSpawnsMixin", + "ServerChunkCacheMixin", + "NaturalSpawnerMixin", + + "DirectionMixin", + "ServerPlayerGameMode_cactusMixin", + "HopperBlock_cactusMixin", + "UseOnContext_cactusMixin", + "DispenserBlock_cactusMixin", + "PistonBaseBlock_rotatorBlockMixin", + "ServerGamePacketListenerImpl_antiCheatDisabledMixin", + "Player_antiCheatDisabledMixin", + "ServerPlayerGameMode_antiCheatMixin", + "HopperBlockEntity_counterMixin", + "AbstractContainerMenu_ctrlQCraftingMixin", + "Connection_packetCounterMixin", + "AbstractCauldronBlock_stackableSBoxesMixin", + "ItemStack_stackableShulkerBoxesMixin", + "ItemEntityMixin", + "TntBlock_noUpdateMixin", + "Explosion_optimizedTntMixin", + "ExplosionAccessor", + "Explosion_scarpetEventMixin", + "Explosion_xpFromBlocksMixin", + "PrimedTntMixin", + "AbstractArrowMixin", + "FallingBlockEntityMixin", + "ThrowableProjectileMixin", + "PathNavigation_pathfindingMixin", + + "InfestedBlock_gravelMixin", + "PistonBaseBlock_qcMixin", + "DispenserBlock_qcMixin", + + "PickaxeItem_missingToolsMixin", + "WitherBoss_moreBlueMixin", + "ArmorStand_scarpetMarkerMixin", + "Entity_scarpetEventsMixin", + "LivingEntity_scarpetEventsMixin", + "Player_scarpetEventsMixin", + "ServerPlayer_scarpetEventMixin", + "AbstractMinecart_scarpetEventsMixin", + "EndCrystal_scarpetEventsMixin", + "FallingBlockEntity_scarpetEventsMixin", + "HangingEntity_scarpetEventsMixin", + "PrimedTnt_scarpetEventsMixin", + "Display_scarpetEventMixin", + "BlockItem_scarpetEventMixin", + "PlayerList_scarpetEventsMixin", + "Inventory_scarpetEventMixin", + "Objective_scarpetMixin", + "Scoreboard_scarpetMixin", + "MerchantResultSlot_scarpetEventMixin", + "AbstractContainerMenu_scarpetMixin", + "AbstractContainerMenuSubclasses_scarpetMixin", + "RecipeBookMenu_scarpetMixin", + "DistanceManager_scarpetMixin", + "PoiRecord_scarpetMixin", + "ServerLevel_scarpetMixin", + "PersistentEntitySectionManager_scarpetMixin", + "ReloadCommand_reloadAppsMixin", + "SystemReport_addScarpetAppsMixin", + "CommandNode_scarpetCommandsMixin", + "CommandDispatcher_scarpetCommandsMixin", + + "RecipeManager_scarpetMixin", + "Ingredient_scarpetMixin", + "BlockInput_scarpetMixin", + "BlockPredicate_scarpetMixin", + "TagPredicate_scarpetMixin", + "HorseBaseEntity_scarpetMixin", + "RandomState_ScarpetMixin", + "Biome_scarpetMixin", + "PortalProcessor_scarpetMixin", + + "BlockEntity_movableBEMixin", + "PistonBaseBlock_movableBEMixin", + "PistonMovingBlockEntity_movableBEMixin", + "Level_movableBEMixin", + "LevelChunk_movableBEMixin", + "Player_creativeNoClipMixin", + "PistonMovingBlockEntity_playerHandlingMixin", + "BlockItem_creativeNoClipMixin", + "StandingAndWallBlockItem_creativeNoClipMixin", + "ShulkerBoxBlockEntity_creativeNoClipMixin", + "TheEndGatewayBlockEntity_creativeNoClipMixin", + "LivingEntity_creativeFlyMixin", + "ChunkMap_creativePlayersLoadChunksMixin", + "SculkSensorBlockEntityVibrationConfig_sculkSensorRangeMixin", + "CollectingNeighborUpdaterAccessor", + + "BlockBehaviourBlockStateBase_mixin", + "ChainBlock_customStickyMixin", + "ChestBlock_customStickyMixin", + "PistonStructureResolver_customStickyMixin", + + "CustomPacketPayload_networkStuffMixin", + "ServerGamePacketListenerimpl_connectionMixin" + + ], + "client": [ + "Minecraft_tickMixin", + "ShulkerBoxAccessMixin", + "MinecraftMixin", + "Gui_tablistMixin", + "PlayerTabOverlayMixin", + "PistonHeadRenderer_movableBEMixin", + "StructureBlockRenderer_mixin", + "ClientPacketListener_clientCommandMixin", + "LevelRenderer_fogOffMixin", + "LevelRenderer_creativeNoClipMixin", + "ClientPacketListener_customPacketsMixin", + "DebugRenderer_scarpetRenderMixin", + "LevelRenderer_scarpetRenderMixin", + + + "ClientCommonPacketListenerImpl_customPacketMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..e1f2e7e --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "id": "carpetpvp", + "version": "${version}", + + "name": "Carpet PVP", + "description": "Carpet made out of fabric", + "authors": [ + "gnembon, TheobaldTheBird" + ], + "contact": { + "homepage": "https://github.com/gnembon/fabric-carpet", + "issues": "https://github.com/gnembon/fabric-carpet/issues", + "sources": "https://github.com/gnembon/fabric-carpet" + }, + + "license": "MIT", + "icon": "assets/carpet/icon.png", + + "environment": "*", + "entrypoints": { + "client": [ + "carpet.CarpetServer::onGameStarted" + ], + "server": [ + "carpet.CarpetServer::onGameStarted", + "carpet.utils.CarpetRulePrinter" + ] + }, + "mixins": [ + "carpet.mixins.json" + ], + "accessWidener" : "carpet.accesswidener", + + "depends": { + "minecraft": ">1.20.1", + "fabricloader": ">=0.14.18", + "java": ">=21" + } +}