From 8e7ec034f246ac9eb553fd5df8958c6161ffca23 Mon Sep 17 00:00:00 2001 From: Feror Date: Mon, 1 Jun 2026 11:07:34 +0200 Subject: [PATCH] CBOT V1.3 --- .gitignore | 9 + LICENSE | 21 + README.md | 123 +++ build.gradle | 53 + gradle.properties | 12 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 46175 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 248 +++++ gradlew.bat | 93 ++ package-lock.json | 954 ++++++++++++++++++ package.json | 12 + scripts/e2e-cbot.mjs | 388 +++++++ settings.gradle | 12 + src/main/java/com/feror/cbot/CbotMod.java | 44 + .../com/feror/cbot/client/CbotClientMod.java | 62 ++ .../com/feror/cbot/client/CbotScreen.java | 256 +++++ .../com/feror/cbot/command/CbotCommand.java | 296 ++++++ .../feror/cbot/network/ActionC2SPayload.java | 63 ++ .../cbot/network/ActionResultS2CPayload.java | 23 + .../feror/cbot/network/CbotNetworking.java | 45 + .../com/feror/cbot/network/CbotStateData.java | 73 ++ .../cbot/network/OpenScreenS2CPayload.java | 20 + .../cbot/network/RequestStateC2SPayload.java | 17 + .../feror/cbot/network/StateS2CPayload.java | 20 + .../cbot/service/CbotOperationResult.java | 5 + .../com/feror/cbot/service/CbotService.java | 192 ++++ .../feror/cbot/state/CbotPersistentState.java | 365 +++++++ .../feror/cbot/util/CommandDispatchUtil.java | 18 + .../com/feror/cbot/util/InventoryUtil.java | 60 ++ .../resources/assets/cbot/lang/en_us.json | 4 + src/main/resources/fabric.mod.json | 29 + 31 files changed, 3524 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/e2e-cbot.mjs create mode 100644 settings.gradle create mode 100644 src/main/java/com/feror/cbot/CbotMod.java create mode 100644 src/main/java/com/feror/cbot/client/CbotClientMod.java create mode 100644 src/main/java/com/feror/cbot/client/CbotScreen.java create mode 100644 src/main/java/com/feror/cbot/command/CbotCommand.java create mode 100644 src/main/java/com/feror/cbot/network/ActionC2SPayload.java create mode 100644 src/main/java/com/feror/cbot/network/ActionResultS2CPayload.java create mode 100644 src/main/java/com/feror/cbot/network/CbotNetworking.java create mode 100644 src/main/java/com/feror/cbot/network/CbotStateData.java create mode 100644 src/main/java/com/feror/cbot/network/OpenScreenS2CPayload.java create mode 100644 src/main/java/com/feror/cbot/network/RequestStateC2SPayload.java create mode 100644 src/main/java/com/feror/cbot/network/StateS2CPayload.java create mode 100644 src/main/java/com/feror/cbot/service/CbotOperationResult.java create mode 100644 src/main/java/com/feror/cbot/service/CbotService.java create mode 100644 src/main/java/com/feror/cbot/state/CbotPersistentState.java create mode 100644 src/main/java/com/feror/cbot/util/CommandDispatchUtil.java create mode 100644 src/main/java/com/feror/cbot/util/InventoryUtil.java create mode 100644 src/main/resources/assets/cbot/lang/en_us.json create mode 100644 src/main/resources/fabric.mod.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40732aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +build/ +out/ +run/ +*.iml +.idea/ +*.class +.DS_Store +node_modules/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +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..69fd7ad --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Carpet Bot Manager (`cbot`) + +Fabric mod that adds player-owned bot management on top of Fabric Carpet's `/player` command. + +The mod can run server-side only for command-based management. If the same jar is installed on both the server and the client, players can also manage their own bots through a GUI. + +## Dependencies + +- Minecraft `1.21.11` (Yarn mapped) +- Fabric Loader +- Fabric API +- Fabric Carpet (for `/player`-based spawn/actions) + +Build system: Gradle + Fabric Loom (project files included). + +If Carpet is missing, `/cbot` still registers, but Carpet-backed actions (`spawn` and forwarded actions) return: +`Carpet mod is not installed`. + +## Client GUI + +Install `cbot` on both the server and client to enable the GUI. + +- `/cbot gui` + - Opens the client GUI when the player has the client mod installed. + - Without the client mod, the server returns `Install cbot on your client to use the GUI`. +- Keybind: `Open Carpet Bot Manager` + - Default is unbound. + - Configure it in Minecraft controls under `Carpet Bot Manager`. + +The GUI is personal bot management only. Admin configuration remains command-only. + +GUI actions are still server-authoritative: + +- The client sends only bot IDs and action parameters. +- The server resolves bot ownership from the executing player. +- Bot actions still dispatch only restricted `player ...` or `tp ...` commands. + +## Commands + +- `/cbot list` + - Shows owned bot IDs, names, and online status. +- `/cbot buy` + - Buys one additional bot using the configured price. +- `/cbot gui` + - Opens the client GUI if the client also has cbot installed. +- `/cbot spawn` + - Runs `player spawn` as server source. +- `/cbot tp` + - Runs `tp `. +- `/cbot tp ` + - Runs `tp `. +- `/cbot ` + - Forwards to `player `. + - Provides autocomplete hints for common Carpet actions while still forwarding the action text unchanged. + +Admin-only config commands require permission level 4: + +- `/cbot config show` + - Shows the current bot name pattern, free bot count, price, and cap. +- `/cbot config botname ""` + - Sets the bot name pattern and renames saved bot identities. + - Supported placeholders: `{player}`, `{id}`, `{uuid}`. + - Generated bot names are sanitized and deterministically shortened to Minecraft's 16-character player-name limit. +- `/cbot config starting-free ` + - Sets how many free bots players are topped up to on next join or `/cbot` use. +- `/cbot config price ` + - Sets the buy price, for example `minecraft:diamond 2`. + - A count of `0` makes buying free. +- `/cbot config cap ` + - Sets the maximum total bots per player. +- `/cbot config cap none` + - Removes the bot cap. +- `/cbot config reset` + - Restores defaults and renames saved bot identities back to the default pattern. + +## Ownership and persistence + +- Ownership is stored per world in a `PersistentState`: + - `Map> botNames`. +- By default, a player gets exactly one starter bot: + - `cbot__1`. +- Additional bots are deterministic and per-owner: + - `cbot__`. +- Defaults: + - Bot name pattern: `cbot_{player}_{id}` + - Starting free bots: `1` + - Price: `1` `minecraft:netherite_block` + - Bot cap: none +- Changing `botname` renames saved identities immediately. Already-spawned old-name fake players are not killed automatically. + +## Security model + +`/cbot` executes underlying commands with elevated server permissions, but only after validation: + +- Executor must be a real `ServerPlayerEntity`. +- Bot ID must resolve from that player's owned list. +- Commands are restricted to: + - `player ...` + - `tp ...` +- All dispatches are logged with owner UUID/name, bot name, action, and command string. + +This prevents arbitrary command execution through `/cbot`. + +## Quick test plan + +1. Fresh world: join as PlayerA, run `/cbot list`, verify 1 starter bot. +2. Run `/cbot 1 spawn`, verify bot appears. +3. Run `/cbot 1 use once`, verify forwarding works. +4. Give netherite block, run `/cbot buy`, verify bot #2 appears in `/cbot list`. +5. Join PlayerB, verify PlayerB cannot manage PlayerA's bot IDs. +6. Restart server, verify ownership persists. +7. As an operator, run `/cbot config starting-free 3`, verify an existing player is topped up on the next `/cbot list`. +8. Run `/cbot config botname "cbot{id}_of_{player}"`, verify saved bot names are renamed and stay spawnable. + +## E2E tests + +Run the Mineflayer-based E2E test with: + +```sh +npm run e2e +``` + +The test starts `./gradlew runServer`, ensures Fabric Carpet is available in `run/mods`, connects real network players with Mineflayer in offline mode, executes `/cbot` commands, checks chat responses, and stops the server. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..14564d8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,53 @@ +plugins { + id "fabric-loom" version "${loom_version}" + id "maven-publish" +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +repositories { + maven { + name = "Fabric" + url = "https://maven.fabricmc.net/" + } + mavenCentral() +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" +} + +processResources { + inputs.property "version", project.version + + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..65d15a3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true + +minecraft_version=1.21.11 +yarn_mappings=1.21.11+build.5 +loader_version=0.19.2 +loom_version=1.15.4 +fabric_version=0.141.4+1.21.11 + +mod_version=1.2.0 +maven_group=com.feror +archives_base_name=cbot diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..61285a659d17295f1de7c53e24fdf13ad755c379 GIT binary patch literal 46175 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

pVX@HRij*jw$g4?}Pn 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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..37f78a6 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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/platforms/jvm/plugins-application/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##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# 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 + + + +# 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +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=SC2039,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=SC2039,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" ) + + 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 + + +# 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"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# 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..e509b2d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +: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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6ae5cde --- /dev/null +++ b/package-lock.json @@ -0,0 +1,954 @@ +{ + "name": "cbot-e2e", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cbot-e2e", + "version": "1.2.0", + "dependencies": { + "mineflayer": "^4.33.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.16.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.1.tgz", + "integrity": "sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.3.tgz", + "integrity": "sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.16.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-rsa": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.4.tgz", + "integrity": "sha512-dB0ECel6JpMnq5ULvpUTunx3yNm8e/dIkv8Zu9p2c8me70xIRUUG3q+qXRwcSf9rN3oqamv4116iHy90dJGRpA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xboxreplay/xboxlive-auth": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@xboxreplay/xboxlive-auth/-/xboxlive-auth-5.1.0.tgz", + "integrity": "sha512-UngHHsehZbiTjyyNmo8HvdoUDKMID1U9uVfrpFWUK/2UxPuVTKy5n+CzZQ3S488sW5vOhgh0lHqqynT8ouwgvw==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/aes-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", + "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/endian-toggle": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/endian-toggle/-/endian-toggle-0.0.0.tgz", + "integrity": "sha512-ShfqhXeHRE4TmggSlHXG8CMGIcsOsqDw/GcoPcosToE59Rm9e4aXaMhEQf2kPBsBRrKem1bbOAv5gOKnkliMFQ==", + "license": "MIT" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "license": "MIT" + }, + "node_modules/macaddress": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.5.4.tgz", + "integrity": "sha512-i8xVWoUjj2woYU8kbpQby86Kq7uF7xl2brtKREXUBWpfgqx1fKXEeYzDiVMVxA/IufC1d3xxwJRHtFCX+9IspA==", + "license": "MIT" + }, + "node_modules/minecraft-data": { + "version": "3.110.2", + "resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.110.2.tgz", + "integrity": "sha512-u0aCCSpQWVreGnZGU/Lu0jmZmc0Y37M0Fvw6eQVQY0BdS/BGRDDU+ug6/qP3QDuZRJCSzi8wNW8ODnOhwpnkpA==", + "license": "MIT" + }, + "node_modules/minecraft-folder-path": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minecraft-folder-path/-/minecraft-folder-path-1.2.0.tgz", + "integrity": "sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw==", + "license": "MIT" + }, + "node_modules/minecraft-protocol": { + "version": "1.66.2", + "resolved": "https://registry.npmjs.org/minecraft-protocol/-/minecraft-protocol-1.66.2.tgz", + "integrity": "sha512-keY1IY1E2AeurcekCfcXrg0TDbykGVFiMe1E4wR8QkNtQRieNwfr2xaF3g3vT9ChkwzvENqp3jxgmtFCKSUKPg==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/node-rsa": "^1.1.4", + "@types/readable-stream": "^4.0.0", + "aes-js": "^3.1.2", + "buffer-equal": "^1.0.0", + "debug": "^4.3.2", + "endian-toggle": "^0.0.0", + "lodash.merge": "^4.3.0", + "minecraft-data": "^3.78.0", + "minecraft-folder-path": "^1.2.0", + "node-fetch": "^2.6.1", + "node-rsa": "^0.4.2", + "prismarine-auth": "^3.1.1", + "prismarine-chat": "^1.10.0", + "prismarine-nbt": "^2.5.0", + "prismarine-realms": "^1.2.0", + "protodef": "^1.17.0", + "readable-stream": "^4.1.0", + "uuid-1345": "^1.0.1", + "yggdrasil": "^1.4.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/mineflayer": { + "version": "4.37.1", + "resolved": "https://registry.npmjs.org/mineflayer/-/mineflayer-4.37.1.tgz", + "integrity": "sha512-kchZCJb1znzz8ZhE0+gLQ3e2t/9xUsqUy/IM/sGfceINxi3h6KXKY9luaUEa59vnD/x0OKwYdERY4sscm0ErNQ==", + "license": "MIT", + "dependencies": { + "minecraft-data": "^3.108.0", + "minecraft-protocol": "^1.66.0", + "mojangson": "^2.0.4", + "prismarine-biome": "^1.1.1", + "prismarine-block": "^1.22.0", + "prismarine-chat": "^1.7.1", + "prismarine-chunk": "^1.39.0", + "prismarine-entity": "^2.5.0", + "prismarine-item": "^1.17.0", + "prismarine-nbt": "^2.0.0", + "prismarine-physics": "^1.9.0", + "prismarine-recipe": "^1.5.0", + "prismarine-registry": "^1.10.0", + "prismarine-windows": "^2.9.0", + "prismarine-world": "^3.6.0", + "protodef": "^1.18.0", + "typed-emitter": "^1.0.0", + "uuid-1345": "^1.0.2", + "vec3": "^0.1.7" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/mojangson": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mojangson/-/mojangson-2.0.4.tgz", + "integrity": "sha512-HYmhgDjr1gzF7trGgvcC/huIg2L8FsVbi/KacRe6r1AswbboGVZDS47SOZlomPuMWvZLas8m9vuHHucdZMwTmQ==", + "license": "MIT", + "dependencies": { + "nearley": "^2.19.5" + } + }, + "node_modules/moo": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz", + "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", + "license": "BSD-3-Clause" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "license": "MIT", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-rsa": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-0.4.2.tgz", + "integrity": "sha512-Bvso6Zi9LY4otIZefYrscsUpo2mUpiAVIEmSZV2q41sP8tHZoert3Yu6zv4f/RXJqMNZQKCtnhDugIuCma23YA==", + "license": "MIT", + "dependencies": { + "asn1": "0.2.3" + } + }, + "node_modules/prismarine-auth": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prismarine-auth/-/prismarine-auth-3.1.1.tgz", + "integrity": "sha512-NuNrMGZdoigFKsvi1ZZgAEvNYNuE5qe6lo/tw+bqeNbkhpjHC0u1JNxLEujnfqduXI18e19PvUtWNMDl/gH7yw==", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^2.0.2", + "@xboxreplay/xboxlive-auth": "^5.1.0", + "debug": "^4.3.3", + "smart-buffer": "^4.1.0", + "uuid-1345": "^1.0.2" + } + }, + "node_modules/prismarine-biome": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prismarine-biome/-/prismarine-biome-1.4.0.tgz", + "integrity": "sha512-fD2WmjN8Zr/xA/jeMInReLgaDlznwA5xlaK529PzWuGzgjpc5ijVu1Lp1oqHyZn3WxOG/bRVtW1bU+tmgCurWA==", + "license": "MIT", + "peerDependencies": { + "prismarine-registry": "^1.1.0" + } + }, + "node_modules/prismarine-block": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/prismarine-block/-/prismarine-block-1.23.0.tgz", + "integrity": "sha512-j2UoU4KbXMvNlBw+aLkMOnEuMayYefznUfbrfv1VIbckG3RA9LpNWltOMHXuOR5YkHp8uIZPOclj95XC88jgGw==", + "license": "MIT", + "dependencies": { + "minecraft-data": "^3.38.0", + "prismarine-biome": "^1.1.0", + "prismarine-chat": "^1.5.0", + "prismarine-item": "^1.10.1", + "prismarine-nbt": "^2.0.0", + "prismarine-registry": "^1.1.0" + } + }, + "node_modules/prismarine-chat": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/prismarine-chat/-/prismarine-chat-1.13.0.tgz", + "integrity": "sha512-tvDbrQmJEoy09yLE5nnedGhQYxnRDaPRePMv7W39dFaHr2LGcA2JfCmH0vG5193+BsEFz3a5+0EpQSK8OW7YmA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.2", + "mojangson": "^2.0.1", + "prismarine-nbt": "^2.0.0", + "prismarine-registry": "^1.4.0" + } + }, + "node_modules/prismarine-chunk": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/prismarine-chunk/-/prismarine-chunk-1.40.0.tgz", + "integrity": "sha512-TtT84Bys7+aGA94HwcK0QDp+jkWcLOLErKYtaWWl+EJya28NqPoBASr5L/lPZ8ZWLQUugg/aFIefZI/rEhEQWw==", + "license": "MIT", + "dependencies": { + "prismarine-biome": "^1.2.0", + "prismarine-block": "^1.14.1", + "prismarine-nbt": "^2.2.1", + "prismarine-registry": "^1.1.0", + "smart-buffer": "^4.1.0", + "uint4": "^0.1.2", + "vec3": "^0.1.3", + "xxhash-wasm": "^0.4.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/prismarine-entity": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-2.6.0.tgz", + "integrity": "sha512-/LlZRLOpACiXk+GqoaKi0XPBFnNMjb1d4OIzuSCSEgNMK6FUo3Wnin5yeSZ7ff3Ztt7yagN9lX2jSOafn6IIzg==", + "license": "MIT", + "dependencies": { + "prismarine-chat": "^1.4.1", + "prismarine-item": "^1.11.2", + "prismarine-registry": "^1.4.0", + "vec3": "^0.1.4" + } + }, + "node_modules/prismarine-item": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/prismarine-item/-/prismarine-item-1.18.0.tgz", + "integrity": "sha512-8pEq6YfcneVvarvUFnex09a3+MR8/4NCQVyawIKAa3kh/g9dHLexoEcpQEgM3cmpg4gbLmspSiARGwed5uGhlg==", + "license": "MIT", + "dependencies": { + "prismarine-nbt": "^2.0.0", + "prismarine-registry": "^1.4.0" + } + }, + "node_modules/prismarine-nbt": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-2.8.0.tgz", + "integrity": "sha512-5D6FUZq0PNtf3v/41ImDlwThVesOv5adyqCRMZLzmkUGEmRJNNh5C6AsnvrClBftXs+IF0yqPnZoj8kcNPiMGg==", + "license": "MIT", + "dependencies": { + "protodef": "^1.18.0" + } + }, + "node_modules/prismarine-physics": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prismarine-physics/-/prismarine-physics-1.11.0.tgz", + "integrity": "sha512-P25VSDi3kJHQAb/AJBiJCQuxyRCVXRSdEiDjx56ywocgt65N/exatVTiJjOK5HgEKHJSfw0sXSAohQhvutnGAA==", + "license": "MIT", + "dependencies": { + "minecraft-data": "^3.0.0", + "prismarine-nbt": "^2.0.0", + "vec3": "^0.1.7" + } + }, + "node_modules/prismarine-realms": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/prismarine-realms/-/prismarine-realms-1.6.0.tgz", + "integrity": "sha512-AwemW0vwxG9hQaFtg1twSV7eymB6QtYxGK0jjpxfdA2sdK15kU8jh8uD1o5XF0oxSMU+BbpzZMCmXtXq4QE6bw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.3", + "node-fetch": "^2.6.1" + } + }, + "node_modules/prismarine-recipe": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prismarine-recipe/-/prismarine-recipe-1.5.0.tgz", + "integrity": "sha512-GRZHbsyBIUgVNF10vFRv2YWZj86vokCT5EWX6iK6gfx6h4FapgZT29V2DNkjv5+hmdzBCLZvfx1/RYr8VPeoGQ==", + "license": "MIT", + "peerDependencies": { + "prismarine-registry": "^1.4.0" + } + }, + "node_modules/prismarine-registry": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prismarine-registry/-/prismarine-registry-1.12.0.tgz", + "integrity": "sha512-OC5U6YrflY6OcAWRZEqe2HGZuNp0bIuP7H+oKEHD6rLfKNDxo8Ymx5eh2VvrZWnMVugpwID1Qj/UjA4MoCzNDw==", + "license": "MIT", + "dependencies": { + "minecraft-data": "^3.70.0", + "prismarine-block": "^1.17.1", + "prismarine-nbt": "^2.0.0" + } + }, + "node_modules/prismarine-windows": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/prismarine-windows/-/prismarine-windows-2.10.0.tgz", + "integrity": "sha512-ssXLGAr7W9JLvvLjYMoo1j4j6AdJaoIb0/HlqkWMWlQqvZJeiS4zyBjJY6+GtR4OzpjkEf6IvF5cNXhHFpbcZQ==", + "license": "MIT", + "dependencies": { + "prismarine-item": "^1.12.2", + "prismarine-registry": "^1.7.0", + "typed-emitter": "^2.1.0" + } + }, + "node_modules/prismarine-windows/node_modules/typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", + "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", + "license": "MIT", + "optionalDependencies": { + "rxjs": "*" + } + }, + "node_modules/prismarine-world": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/prismarine-world/-/prismarine-world-3.7.0.tgz", + "integrity": "sha512-M5euvNjQ3vIk689BSa0YC6PBwpVY35Oc6q6KyZ0IqyFtI+cQ9em+8l5OTAK/uu9/gzDDhR7cmm9L2WXgTXBQCw==", + "license": "MIT", + "dependencies": { + "vec3": "^0.1.7" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/protodef": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/protodef/-/protodef-1.19.0.tgz", + "integrity": "sha512-94f3GR7pk4Qi5YVLaLvWBfTGUIzzO8hyo7vFVICQuu5f5nwKtgGDaeC1uXIu49s5to/49QQhEYeL0aigu1jEGA==", + "license": "MIT", + "dependencies": { + "lodash.reduce": "^4.6.0", + "protodef-validator": "^1.3.0", + "readable-stream": "^4.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/protodef-validator": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/protodef-validator/-/protodef-validator-1.4.0.tgz", + "integrity": "sha512-2y2coBolqCEuk5Kc3QwO7ThR+/7TZiOit4FrpAgl+vFMvq8w76nDhh09z08e2NQOdrgPLsN2yzXsvRvtADgUZQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.5.4" + }, + "bin": { + "protodef-validator": "cli.js" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "license": "CC0-1.0" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "license": "MIT", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/typed-emitter": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-1.4.0.tgz", + "integrity": "sha512-weBmoo3HhpKGgLBOYwe8EB31CzDFuaK7CCL+axXhUYhn4jo6DSkHnbefboCF5i4DQ2aMFe0C/FdTWcPdObgHyg==", + "license": "MIT" + }, + "node_modules/uint4": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uint4/-/uint4-0.1.2.tgz", + "integrity": "sha512-lhEx78gdTwFWG+mt6cWAZD/R6qrIj0TTBeH5xwyuDJyswLNlGe+KVlUPQ6+mx5Ld332pS0AMUTo9hIly7YsWxQ==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uuid-1345": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uuid-1345/-/uuid-1345-1.0.2.tgz", + "integrity": "sha512-bA5zYZui+3nwAc0s3VdGQGBfbVsJLVX7Np7ch2aqcEWFi5lsAEcmO3+lx3djM1npgpZI8KY2FITZ2uYTnYUYyw==", + "license": "MIT", + "dependencies": { + "macaddress": "^0.5.1" + } + }, + "node_modules/vec3": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz", + "integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==", + "license": "BSD" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xxhash-wasm": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz", + "integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==", + "license": "MIT" + }, + "node_modules/yggdrasil": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/yggdrasil/-/yggdrasil-1.8.0.tgz", + "integrity": "sha512-r5bKOhkZ52DJ6q034uSkdsdZLoFVhOmfDOagRs6h/JX5W7+XIPOMb+peCbElhLEoIckwt43NCUoNQbydOzuPcQ==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "uuid": "^10.0.0" + } + }, + "node_modules/yggdrasil/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..df042e4 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "cbot-e2e", + "version": "1.2.0", + "private": true, + "type": "module", + "scripts": { + "e2e": "node scripts/e2e-cbot.mjs" + }, + "dependencies": { + "mineflayer": "^4.33.0" + } +} diff --git a/scripts/e2e-cbot.mjs b/scripts/e2e-cbot.mjs new file mode 100644 index 0000000..c330468 --- /dev/null +++ b/scripts/e2e-cbot.mjs @@ -0,0 +1,388 @@ +import { createWriteStream, existsSync } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import https from "node:https"; +import net from "node:net"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import mineflayer from "mineflayer"; + +const ROOT = path.resolve(new URL("..", import.meta.url).pathname); +const RUN_DIR = path.join(ROOT, "run"); +const MODS_DIR = path.join(RUN_DIR, "mods"); +const CARPET_JAR = "fabric-carpet-1.21.11-1.4.194+v251223.jar"; +const CARPET_URL = "https://github.com/gnembon/fabric-carpet/releases/download/1.4.194/fabric-carpet-1.21.11-1.4.194%2Bv251223.jar"; +const MC_VERSION = "1.21.11"; +const TEST_TIMEOUT_MS = 180_000; +const CHAT_TIMEOUT_MS = 15_000; + +const startedAt = Date.now(); +let serverProcess; +const clients = []; + +async function main() { + const port = Number(process.env.CBOT_E2E_PORT || await getFreePort()); + const suffix = Date.now().toString(36).slice(-5); + const playerA = `A${suffix}`; + const playerB = `B${suffix}`; + + await prepareRunDirectory(port); + + serverProcess = startServer(); + await waitForServerReady(serverProcess); + + assertIncludes(serverProcess.output, "- carpet ", "server loaded Fabric Carpet"); + assertIncludes(serverProcess.output, "- cbot ", "server loaded cbot"); + serverCommand("cbot config reset"); + await delay(750); + + const a = await connectPlayer(playerA, port); + await runAndExpect(a, "cbot list", [`#1 cbot_${playerA}_1 [offline]`]); + await runAndExpect(a, "cbot gui", ["Install cbot on your client to use the GUI"]); + await runAndExpect(a, "cbot buy", ["You need 1 minecraft:netherite_block"]); + + serverCommand(`give ${playerA} netherite_block 1`); + await delay(750); + await runAndExpect(a, "cbot buy", [`Bought bot #2 (cbot_${playerA}_2)`]); + await runAndExpect(a, "cbot list", [`#2 cbot_${playerA}_2 [offline]`]); + + const b = await connectPlayer(playerB, port); + await runAndExpectEither(b, "cbot config show", ["Unknown or incomplete command", "You do not have permission", "Incorrect argument for command"]); + + serverCommand(`op ${playerA}`); + await delay(750); + await runAndExpect(a, "cbot config show", ["cbot config:", "starting-free: 1", "price: minecraft:netherite_block x1", "cap: none"]); + await runAndExpect(a, "cbot config starting-free 3", ["Set starting free bots to 3"]); + await runAndExpect(a, "cbot list", [`#3 cbot_${playerA}_3 [offline]`]); + + await runAndExpect(a, "cbot config price minecraft:diamond 2", ["Set bot price to minecraft:diamond x2"]); + await runAndExpect(a, "cbot buy", ["You need 2 minecraft:diamond"]); + serverCommand(`give ${playerA} diamond 2`); + await delay(750); + await runAndExpect(a, "cbot buy", [`Bought bot #4 (cbot_${playerA}_4)`]); + + await runAndExpect(a, "cbot config price minecraft:diamond 0", ["Set bot price to minecraft:diamond x0"]); + await runAndExpect(a, "cbot buy", [`Bought bot #5 (cbot_${playerA}_5)`]); + await runAndExpect(a, "cbot config cap 5", ["Set bot cap to 5"]); + await runAndExpect(a, "cbot buy", ["You have reached the bot cap"]); + await runAndExpect(a, "cbot config cap 2", ["Set bot cap to 2"]); + await runAndExpect(a, "cbot list", [`#5 cbot_${playerA}_5 [offline]`]); + + const completions = await a.tabComplete("/cbot 1 u", true, false, 5000); + assertIncludes(normalizeCompletions(completions).join("\n"), "use", "action autocomplete includes use"); + + await runAndExpect(a, "cbot config cap none", ["Set bot cap to none"]); + await runAndExpect(a, "cbot config botname \"cbot{id}_of_{player}\"", ["Set botname pattern to: cbot{id}_of_{player}", "Already-spawned old-name bots may need"]); + const renamedA = await runAndCollect(a, "cbot list", ["Your bots:"]); + assertIncludes(renamedA.join("\n"), `#1 cbot1_of_${playerA} [offline]`, "renamed bot list reflects pattern"); + assertNotIncludes(renamedA.join("\n"), `cbot_${playerA}_1`, "old bot name no longer listed after rename"); + const renamedANames = parseListedBotNames(renamedA); + + const longPlayer = `LongName${suffix}ZZZ`.slice(0, 16); + const c = await connectPlayer(longPlayer, port); + const longList = await runAndCollect(c, "cbot list", ["Your bots:", "#3 "]); + const longNames = parseListedBotNames(longList); + if (longNames.length < 3) { + throw new Error(`expected at least 3 long-player bots; got ${longNames.join(", ")}`); + } + for (const name of longNames) { + if (name.length > 16) { + throw new Error(`expected truncated bot name <= 16 chars; got ${name}`); + } + } + await runAndExpect(c, "cbot 1 spawn", [`Spawned ${longNames[0]}`]); + await waitForChatIncludes(c, `${longNames[0]} joined the game`, 30_000); + + await runAndExpect(a, "cbot 1 spawn", [`Spawned ${renamedANames[0]}`]); + await waitForChatIncludes(a, `${renamedANames[0]} joined the game`, 30_000); + await runAndExpect(a, "cbot list", [`#1 ${renamedANames[0]} [online]`]); + await runAndExpect(a, "cbot 1 use once", [`Sent action to ${renamedANames[0]}`]); + await runAndExpect(a, "cbot 1 tp", [`Teleported ${renamedANames[0]} to you`]); + await runAndExpect(a, "cbot 1 tp 10 80 10", [`Teleported ${renamedANames[0]}`]); + + await runAndExpect(a, "cbot config reset", ["Reset cbot config to defaults"]); + await runAndExpect(b, "cbot list", [`#1 cbot_${playerB}_1 [offline]`]); + await runAndExpect(b, "cbot 2 spawn", ["Invalid bot id"]); + + await stopServer(); + console.log(`[e2e] passed in ${Math.round((Date.now() - startedAt) / 1000)}s`); +} + +async function prepareRunDirectory(port) { + await mkdir(MODS_DIR, { recursive: true }); + await ensureCarpetJar(); + await writeFile(path.join(RUN_DIR, "eula.txt"), "eula=true\n", "utf8"); + await patchServerProperties(port); +} + +async function ensureCarpetJar() { + const jarPath = path.join(MODS_DIR, CARPET_JAR); + if (existsSync(jarPath)) { + return; + } + + console.log(`[e2e] downloading ${CARPET_JAR}`); + await download(CARPET_URL, jarPath); +} + +async function patchServerProperties(port) { + const file = path.join(RUN_DIR, "server.properties"); + const existing = existsSync(file) ? await readFile(file, "utf8") : ""; + const values = new Map(); + + for (const line of existing.split(/\r?\n/)) { + if (!line || line.startsWith("#") || !line.includes("=")) { + continue; + } + const [key, ...rest] = line.split("="); + values.set(key, rest.join("=")); + } + + values.set("server-port", String(port)); + values.set("online-mode", "false"); + values.set("enforce-secure-profile", "false"); + values.set("spawn-protection", "0"); + values.set("white-list", "false"); + values.set("enable-command-block", "true"); + + const body = [ + "#Minecraft server properties for cbot e2e tests", + ...[...values.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`), + "", + ].join("\n"); + await writeFile(file, body, "utf8"); +} + +function startServer() { + console.log("[e2e] starting Fabric server"); + const child = spawn("./gradlew", ["runServer"], { + cwd: ROOT, + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env, TERM: "dumb" }, + }); + + child.output = ""; + child.stdout.on("data", (chunk) => { + const text = chunk.toString(); + child.output += text; + process.stdout.write(prefixLines(text, "[server] ")); + }); + child.stderr.on("data", (chunk) => { + const text = chunk.toString(); + child.output += text; + process.stderr.write(prefixLines(text, "[server] ")); + }); + + return child; +} + +async function waitForServerReady(child) { + await withTimeout(new Promise((resolve, reject) => { + const onExit = (code) => reject(new Error(`server exited before ready with code ${code}`)); + const onData = () => { + if (child.output.includes("Done (") && child.output.includes("For help, type \"help\"")) { + child.off("exit", onExit); + child.stdout.off("data", onData); + child.stderr.off("data", onData); + resolve(); + } + }; + + child.on("exit", onExit); + child.stdout.on("data", onData); + child.stderr.on("data", onData); + }), TEST_TIMEOUT_MS, "server startup"); +} + +async function connectPlayer(username, port) { + console.log(`[e2e] connecting ${username}`); + const bot = mineflayer.createBot({ + host: "127.0.0.1", + port, + username, + auth: "offline", + version: MC_VERSION, + }); + clients.push(bot); + + bot.seenMessages = []; + bot.on("message", (message) => { + const text = message.toString(); + bot.seenMessages.push(text); + console.log(`[${username}] ${text}`); + }); + + await withTimeout(new Promise((resolve, reject) => { + bot.once("spawn", resolve); + bot.once("error", reject); + bot.once("kicked", (reason) => reject(new Error(`${username} kicked: ${reason}`))); + }), CHAT_TIMEOUT_MS, `${username} login`); + + await delay(1000); + return bot; +} + +async function runAndExpect(bot, command, expectedMessages) { + const start = bot.seenMessages.length; + console.log(`[e2e] /${command}`); + bot.chat(`/${command}`); + + await withTimeout(waitUntil(() => { + const received = bot.seenMessages.slice(start); + return expectedMessages.every((expected) => received.some((message) => message.includes(expected))); + }), CHAT_TIMEOUT_MS, `/${command} -> ${expectedMessages.join(", ")}`); +} + +async function runAndExpectEither(bot, command, expectedMessages) { + const start = bot.seenMessages.length; + console.log(`[e2e] /${command}`); + bot.chat(`/${command}`); + + await withTimeout(waitUntil(() => { + const received = bot.seenMessages.slice(start); + return expectedMessages.some((expected) => received.some((message) => message.includes(expected))); + }), CHAT_TIMEOUT_MS, `/${command} -> one of ${expectedMessages.join(", ")}`); +} + +async function runAndCollect(bot, command, expectedMessages) { + const start = bot.seenMessages.length; + console.log(`[e2e] /${command}`); + bot.chat(`/${command}`); + + await withTimeout(waitUntil(() => { + const received = bot.seenMessages.slice(start); + return expectedMessages.every((expected) => received.some((message) => message.includes(expected))); + }), CHAT_TIMEOUT_MS, `/${command} -> ${expectedMessages.join(", ")}`); + + return bot.seenMessages.slice(start); +} + +async function waitForChatIncludes(bot, expected, timeoutMs = CHAT_TIMEOUT_MS) { + await withTimeout(waitUntil(() => { + return bot.seenMessages.some((message) => message.includes(expected)); + }), timeoutMs, `chat message containing "${expected}"`); +} + +function serverCommand(command) { + console.log(`[console] ${command}`); + serverProcess.stdin.write(`${command}\n`); +} + +async function stopServer() { + for (const bot of clients) { + bot.quit(); + } + + if (!serverProcess || serverProcess.exitCode !== null) { + return; + } + + console.log("[e2e] stopping server"); + serverProcess.stdin.write("stop\n"); + await withTimeout(new Promise((resolve) => serverProcess.once("exit", resolve)), 30_000, "server stop"); +} + +async function waitUntil(predicate) { + while (!predicate()) { + await delay(100); + } +} + +async function getFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, "127.0.0.1", () => { + const { port } = server.address(); + server.close(() => resolve(port)); + }); + server.on("error", reject); + }); +} + +async function download(url, destination) { + await new Promise((resolve, reject) => { + const request = https.get(url, (response) => { + if ([301, 302, 303, 307, 308].includes(response.statusCode)) { + response.resume(); + download(response.headers.location, destination).then(resolve, reject); + return; + } + + if (response.statusCode !== 200) { + response.resume(); + reject(new Error(`download failed with HTTP ${response.statusCode}`)); + return; + } + + const file = createWriteStream(destination); + response.pipe(file); + file.on("finish", () => file.close(resolve)); + file.on("error", reject); + }); + request.on("error", reject); + }); +} + +async function withTimeout(promise, timeoutMs, label) { + let timeout; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), timeoutMs); + }), + ]); + } finally { + clearTimeout(timeout); + } +} + +function assertIncludes(haystack, needle, label) { + if (!haystack.includes(needle)) { + throw new Error(`expected ${label}; missing "${needle}"`); + } +} + +function assertNotIncludes(haystack, needle, label) { + if (haystack.includes(needle)) { + throw new Error(`expected ${label}; unexpectedly found "${needle}"`); + } +} + +function parseListedBotNames(messages) { + const names = []; + for (const message of messages) { + const match = message.match(/^#\d+ (\S+) \[(?:online|offline)\]$/); + if (match) { + names.push(match[1]); + } + } + return names; +} + +function normalizeCompletions(completions) { + return completions.map((completion) => { + if (typeof completion === "string") { + return completion; + } + return completion.match ?? completion.text ?? String(completion); + }); +} + +function prefixLines(text, prefix) { + return text.split(/(?<=\n)/).map((line) => line ? `${prefix}${line}` : line).join(""); +} + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +process.on("SIGINT", async () => { + await stopServer().catch(() => {}); + process.exit(130); +}); + +main().catch(async (error) => { + console.error(`[e2e] failed: ${error.stack || error.message}`); + await stopServer().catch(() => {}); + process.exit(1); +}); diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5fc8e22 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = "Fabric" + url = "https://maven.fabricmc.net/" + } + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "cbot" diff --git a/src/main/java/com/feror/cbot/CbotMod.java b/src/main/java/com/feror/cbot/CbotMod.java new file mode 100644 index 0000000..3746db2 --- /dev/null +++ b/src/main/java/com/feror/cbot/CbotMod.java @@ -0,0 +1,44 @@ +package com.feror.cbot; + +import com.feror.cbot.command.CbotCommand; +import com.feror.cbot.network.CbotNetworking; +import com.feror.cbot.state.CbotPersistentState; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.server.network.ServerPlayerEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CbotMod implements ModInitializer { + public static final String MOD_ID = "cbot"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + @Override + public void onInitialize() { + CbotNetworking.registerPayloads(); + CbotNetworking.registerServerReceivers(); + CbotCommand.register(); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + if (isFakePlayer(handler.player)) { + return; + } + CbotPersistentState state = CbotPersistentState.get(server); + state.ensureFreeBots(handler.player.getUuid(), handler.player.getStringifiedName()); + }); + } + + private static boolean isFakePlayer(ServerPlayerEntity player) { + String className = player.getClass().getName().toLowerCase(); + if (className.contains("fake")) { + return true; + } + + try { + Class carpetFakeClass = Class.forName("carpet.patches.EntityPlayerMPFake"); + return carpetFakeClass.isInstance(player); + } catch (ClassNotFoundException ignored) { + return false; + } + } +} diff --git a/src/main/java/com/feror/cbot/client/CbotClientMod.java b/src/main/java/com/feror/cbot/client/CbotClientMod.java new file mode 100644 index 0000000..744eafd --- /dev/null +++ b/src/main/java/com/feror/cbot/client/CbotClientMod.java @@ -0,0 +1,62 @@ +package com.feror.cbot.client; + +import com.feror.cbot.CbotMod; +import com.feror.cbot.network.ActionResultS2CPayload; +import com.feror.cbot.network.CbotStateData; +import com.feror.cbot.network.OpenScreenS2CPayload; +import com.feror.cbot.network.RequestStateC2SPayload; +import com.feror.cbot.network.StateS2CPayload; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +public class CbotClientMod implements ClientModInitializer { + private static KeyBinding openKey; + + @Override + public void onInitializeClient() { + openKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.cbot.open_gui", + InputUtil.Type.KEYSYM, + InputUtil.UNKNOWN_KEY.getCode(), + KeyBinding.Category.create(Identifier.of(CbotMod.MOD_ID, "keybindings")) + )); + + ClientTickEvents.END_CLIENT_TICK.register(client -> { + while (openKey.wasPressed()) { + requestOpen(client); + } + }); + + ClientPlayNetworking.registerGlobalReceiver(OpenScreenS2CPayload.ID, (payload, context) -> + context.client().setScreen(new CbotScreen(payload.state())) + ); + ClientPlayNetworking.registerGlobalReceiver(StateS2CPayload.ID, (payload, context) -> updateOpenScreen(context.client(), payload.state())); + ClientPlayNetworking.registerGlobalReceiver(ActionResultS2CPayload.ID, (payload, context) -> updateOpenScreen(context.client(), payload.state())); + } + + public static void requestOpen(MinecraftClient client) { + if (client.player == null) { + return; + } + if (!ClientPlayNetworking.canSend(RequestStateC2SPayload.ID)) { + client.player.sendMessage(Text.literal("This server does not support the cbot GUI"), false); + return; + } + ClientPlayNetworking.send(new RequestStateC2SPayload()); + } + + private static void updateOpenScreen(MinecraftClient client, CbotStateData state) { + if (client.currentScreen instanceof CbotScreen cbotScreen) { + cbotScreen.setState(state); + } else if (!state.message().isBlank() && client.player != null) { + client.player.sendMessage(Text.literal(state.message()), false); + } + } +} diff --git a/src/main/java/com/feror/cbot/client/CbotScreen.java b/src/main/java/com/feror/cbot/client/CbotScreen.java new file mode 100644 index 0000000..13a0d51 --- /dev/null +++ b/src/main/java/com/feror/cbot/client/CbotScreen.java @@ -0,0 +1,256 @@ +package com.feror.cbot.client; + +import com.feror.cbot.network.ActionC2SPayload; +import com.feror.cbot.network.CbotStateData; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; + +import java.util.List; + +public class CbotScreen extends Screen { + private static final int PANEL = 180; + private static final int BUTTON_W = 118; + private static final int BUTTON_H = 20; + private static final int LOGICAL_WIDTH = 530; + private static final int LOGICAL_HEIGHT = 326; + private static final int SCREEN_PADDING = 8; + + private CbotStateData state; + private int selectedBotId = 1; + private float contentScale = 1.0F; + private float contentX = 0.0F; + private float contentY = 0.0F; + private TextFieldWidget xField; + private TextFieldWidget yField; + private TextFieldWidget zField; + private TextFieldWidget customActionField; + + public CbotScreen(CbotStateData state) { + super(Text.literal("Carpet Bots")); + this.state = state; + } + + public void setState(CbotStateData state) { + this.state = state; + if (state.bots().stream().noneMatch(bot -> bot.id() == selectedBotId) && !state.bots().isEmpty()) { + selectedBotId = state.bots().getFirst().id(); + } + clearAndInit(); + } + + @Override + protected void init() { + updateContentTransform(); + + int left = 16; + int y = 36; + for (CbotStateData.BotData bot : state.bots()) { + ButtonWidget button = ButtonWidget.builder(Text.literal(botLabel(bot)), ignored -> { + selectedBotId = bot.id(); + clearAndInit(); + }).dimensions(left, y, PANEL - 24, BUTTON_H).build(); + button.active = bot.id() != selectedBotId; + addDrawableChild(button); + y += 24; + } + + int right = PANEL + 24; + int row = 58; + addButton("Refresh", right, row, () -> send(ActionC2SPayload.refresh()), true); + addButton("Buy Bot", right + 124, row, () -> send(ActionC2SPayload.buy()), state.canBuy()); + + CbotStateData.BotData selected = selectedBot(); + boolean hasSelection = selected != null; + boolean selectedOnline = selected != null && selected.online(); + boolean carpet = state.carpetLoaded(); + + row += 28; + addButton("Spawn", right, row, () -> send(ActionC2SPayload.bot(ActionC2SPayload.Action.SPAWN, selectedBotId)), hasSelection && carpet && !selectedOnline); + addButton("TP to Me", right + 124, row, () -> send(ActionC2SPayload.bot(ActionC2SPayload.Action.TP_TO_SELF, selectedBotId)), hasSelection && selectedOnline); + + row += 32; + xField = textField(right, row, 56, "X"); + yField = textField(right + 62, row, 56, "Y"); + zField = textField(right + 124, row, 56, "Z"); + addDrawableChild(xField); + addDrawableChild(yField); + addDrawableChild(zField); + addButton("TP to Coordinates", right + 186, row, this::sendTpToCoords, hasSelection && selectedOnline); + + row += 36; + addPreset(right, row, "Stop", "stop", hasSelection && carpet && selectedOnline); + addPreset(right + 124, row, "Kill", "kill", hasSelection && carpet && selectedOnline); + row += 24; + addPreset(right, row, "Use Once", "use once", hasSelection && carpet && selectedOnline); + addPreset(right + 124, row, "Use Cont.", "use continuous", hasSelection && carpet && selectedOnline); + row += 24; + addPreset(right, row, "Attack Once", "attack once", hasSelection && carpet && selectedOnline); + addPreset(right + 124, row, "Attack Cont.", "attack continuous", hasSelection && carpet && selectedOnline); + row += 24; + addPreset(right, row, "Jump", "jump", hasSelection && carpet && selectedOnline); + addPreset(right + 124, row, "Sneak", "sneak", hasSelection && carpet && selectedOnline); + row += 24; + addPreset(right, row, "Unsneak", "unsneak", hasSelection && carpet && selectedOnline); + addPreset(right + 124, row, "Sprint", "sprint", hasSelection && carpet && selectedOnline); + row += 24; + addPreset(right, row, "Unsprint", "unsprint", hasSelection && carpet && selectedOnline); + + row += 34; + customActionField = new TextFieldWidget(textRenderer, right, row, 180, 20, Text.literal("Custom Carpet action")); + customActionField.setPlaceholder(Text.literal("e.g. move forward")); + customActionField.setMaxLength(120); + addDrawableChild(customActionField); + addButton("Send Custom", right + 186, row, this::sendCustomAction, hasSelection && carpet && selectedOnline); + } + + private TextFieldWidget textField(int x, int y, int width, String placeholder) { + TextFieldWidget field = new TextFieldWidget(textRenderer, x, y, width, 20, Text.literal(placeholder)); + field.setPlaceholder(Text.literal(placeholder)); + field.setMaxLength(16); + return field; + } + + private void addPreset(int x, int y, String label, String action, boolean active) { + addButton(label, x, y, () -> send(ActionC2SPayload.forward(selectedBotId, action)), active); + } + + private void addButton(String label, int x, int y, Runnable action, boolean active) { + ButtonWidget button = ButtonWidget.builder(Text.literal(label), ignored -> action.run()) + .dimensions(x, y, BUTTON_W, BUTTON_H) + .build(); + button.active = active; + addDrawableChild(button); + } + + private void sendTpToCoords() { + try { + double x = Double.parseDouble(xField.getText()); + double y = Double.parseDouble(yField.getText()); + double z = Double.parseDouble(zField.getText()); + send(ActionC2SPayload.tpToCoords(selectedBotId, x, y, z)); + } catch (NumberFormatException exception) { + setState(state.withMessage("Invalid coordinates")); + } + } + + private void sendCustomAction() { + String action = customActionField.getText().trim(); + if (action.isEmpty()) { + setState(state.withMessage("Enter a Carpet action first")); + return; + } + send(ActionC2SPayload.forward(selectedBotId, action)); + } + + private void send(ActionC2SPayload payload) { + if (!ClientPlayNetworking.canSend(ActionC2SPayload.ID)) { + setState(state.withMessage("This server does not support the cbot GUI")); + return; + } + ClientPlayNetworking.send(payload); + } + + private CbotStateData.BotData selectedBot() { + return state.bots().stream().filter(bot -> bot.id() == selectedBotId).findFirst().orElse(null); + } + + private static String botLabel(CbotStateData.BotData bot) { + return "#" + bot.id() + " " + bot.name() + " [" + (bot.online() ? "online" : "offline") + "]"; + } + + private void updateContentTransform() { + float availableWidth = Math.max(1.0F, width - SCREEN_PADDING); + float availableHeight = Math.max(1.0F, height - SCREEN_PADDING); + contentScale = Math.min(1.0F, Math.min(availableWidth / LOGICAL_WIDTH, availableHeight / LOGICAL_HEIGHT)); + contentX = (width - LOGICAL_WIDTH * contentScale) / 2.0F; + contentY = (height - LOGICAL_HEIGHT * contentScale) / 2.0F; + } + + private double toLogicalX(double x) { + return (x - contentX) / contentScale; + } + + private double toLogicalY(double y) { + return (y - contentY) / contentScale; + } + + private Click toLogicalClick(Click click) { + return new Click(toLogicalX(click.x()), toLogicalY(click.y()), click.buttonInfo()); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(0, 0, width, height, 0xC0101010); + + double logicalMouseX = toLogicalX(mouseX); + double logicalMouseY = toLogicalY(mouseY); + context.getMatrices().pushMatrix(); + context.getMatrices().translate(contentX, contentY); + context.getMatrices().scale(contentScale); + + context.fill(10, 26, PANEL, LOGICAL_HEIGHT - 18, 0xAA111111); + context.fill(PANEL + 12, 26, LOGICAL_WIDTH - 10, LOGICAL_HEIGHT - 18, 0xAA111111); + context.drawCenteredTextWithShadow(textRenderer, title, LOGICAL_WIDTH / 2, 10, 0xFFFFFF); + context.drawTextWithShadow(textRenderer, "Your bots", 18, 18, 0xE0E0E0); + context.drawTextWithShadow(textRenderer, "Server policy", PANEL + 24, 18, 0xE0E0E0); + + int infoX = PANEL + 24; + int infoY = 34; + context.drawTextWithShadow(textRenderer, "Price: " + state.priceItemId() + " x" + state.priceCount(), infoX, infoY, 0xD0D0D0); + context.drawTextWithShadow(textRenderer, "Cap: " + state.capDisplay() + " | Free: " + state.startingFreeBots(), infoX, infoY + 10, 0xD0D0D0); + context.drawTextWithShadow(textRenderer, "Funds: " + fundsLabel(), infoX, infoY + 20, state.hasCurrency() ? 0x80FF80 : 0xFF8080); + context.drawTextWithShadow(textRenderer, "Carpet: " + (state.carpetLoaded() ? "installed" : "missing"), infoX, infoY + 30, state.carpetLoaded() ? 0x80FF80 : 0xFF8080); + + CbotStateData.BotData selected = selectedBot(); + String selection = selected == null ? "No bot selected" : "Selected: #" + selected.id() + " " + selected.name(); + context.drawTextWithShadow(textRenderer, selection, infoX, infoY + 206, 0xE0E0E0); + if (!state.message().isBlank()) { + context.drawTextWithShadow(textRenderer, state.message(), infoX, LOGICAL_HEIGHT - 32, state.capReached() ? 0xFFDD80 : 0xFFFF80); + } + + super.render(context, (int) logicalMouseX, (int) logicalMouseY, delta); + context.getMatrices().popMatrix(); + } + + private String fundsLabel() { + if (state.priceCount() <= 0) { + return "free"; + } + return state.hasCurrency() ? "ready" : "missing"; + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + super.mouseMoved(toLogicalX(mouseX), toLogicalY(mouseY)); + } + + @Override + public boolean mouseClicked(Click click, boolean doubled) { + return super.mouseClicked(toLogicalClick(click), doubled); + } + + @Override + public boolean mouseReleased(Click click) { + return super.mouseReleased(toLogicalClick(click)); + } + + @Override + public boolean mouseDragged(Click click, double offsetX, double offsetY) { + return super.mouseDragged(toLogicalClick(click), offsetX / contentScale, offsetY / contentScale); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + return super.mouseScrolled(toLogicalX(mouseX), toLogicalY(mouseY), horizontalAmount, verticalAmount); + } + + @Override + public boolean shouldPause() { + return false; + } +} diff --git a/src/main/java/com/feror/cbot/command/CbotCommand.java b/src/main/java/com/feror/cbot/command/CbotCommand.java new file mode 100644 index 0000000..30f23c6 --- /dev/null +++ b/src/main/java/com/feror/cbot/command/CbotCommand.java @@ -0,0 +1,296 @@ +package com.feror.cbot.command; + +import com.feror.cbot.CbotMod; +import com.feror.cbot.network.CbotNetworking; +import com.feror.cbot.network.OpenScreenS2CPayload; +import com.feror.cbot.service.CbotOperationResult; +import com.feror.cbot.service.CbotService; +import com.feror.cbot.state.CbotPersistentState; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.command.CommandSource; +import net.minecraft.command.argument.IdentifierArgumentType; +import net.minecraft.registry.Registries; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; + +import java.util.List; + +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public final class CbotCommand { + private static final String CARPET_MOD_ID = "carpet"; + private static final SimpleCommandExceptionType PLAYER_ONLY = new SimpleCommandExceptionType(Text.literal("Only players can use /cbot")); + private static final SimpleCommandExceptionType REAL_PLAYER_ONLY = new SimpleCommandExceptionType(Text.literal("Only real players can use /cbot")); + private static final String[] CARPET_ACTION_HINTS = { + "attack", "attack once", "attack continuous", "attack interval", + "use", "use once", "use continuous", "use interval", + "stop", "kill", "mount", "dismount", "drop", "dropStack", "jump", + "look", "move", "move forward", "move backward", "move left", "move right", + "sneak", "unsneak", "sprint", "unsprint", "swapHands", "turn" + }; + + private CbotCommand() {} + + public static void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> + register(dispatcher) + ); + } + + private static void register(CommandDispatcher dispatcher) { + dispatcher.register( + literal("cbot") + .then(literal("config") + .requires(CommandManager.requirePermissionLevel(CommandManager.OWNERS_CHECK)) + .then(literal("show") + .executes(context -> showConfig(context.getSource()))) + .then(literal("botname") + .then(argument("pattern", StringArgumentType.string()) + .executes(context -> setBotNamePattern( + context.getSource(), + StringArgumentType.getString(context, "pattern") + )))) + .then(literal("starting-free") + .then(argument("count", IntegerArgumentType.integer(0)) + .executes(context -> setStartingFreeBots( + context.getSource(), + IntegerArgumentType.getInteger(context, "count") + )))) + .then(literal("price") + .then(argument("item", IdentifierArgumentType.identifier()) + .suggests((context, builder) -> CommandSource.suggestIdentifiers(Registries.ITEM.getIds(), builder)) + .then(argument("count", IntegerArgumentType.integer(0)) + .executes(context -> setPrice( + context.getSource(), + IdentifierArgumentType.getIdentifier(context, "item"), + IntegerArgumentType.getInteger(context, "count") + ))))) + .then(literal("cap") + .then(literal("none") + .executes(context -> setBotCap(context.getSource(), CbotPersistentState.UNLIMITED_BOT_CAP))) + .then(argument("count", IntegerArgumentType.integer(0)) + .executes(context -> setBotCap( + context.getSource(), + IntegerArgumentType.getInteger(context, "count") + )))) + .then(literal("reset") + .executes(context -> resetConfig(context.getSource())))) + .then(literal("list") + .executes(context -> listBots(context.getSource()))) + .then(literal("buy") + .executes(context -> buyBot(context.getSource()))) + .then(literal("gui") + .executes(context -> openGui(context.getSource()))) + .then(argument("id", IntegerArgumentType.integer(1)) + .then(literal("spawn") + .executes(context -> spawnBot(context.getSource(), IntegerArgumentType.getInteger(context, "id")))) + .then(literal("tp") + .executes(context -> tpToOwner(context.getSource(), IntegerArgumentType.getInteger(context, "id"))) + .then(argument("x", DoubleArgumentType.doubleArg()) + .then(argument("y", DoubleArgumentType.doubleArg()) + .then(argument("z", DoubleArgumentType.doubleArg()) + .executes(context -> tpToCoords( + context.getSource(), + IntegerArgumentType.getInteger(context, "id"), + DoubleArgumentType.getDouble(context, "x"), + DoubleArgumentType.getDouble(context, "y"), + DoubleArgumentType.getDouble(context, "z") + )))))) + .then(argument("action", StringArgumentType.greedyString()) + .suggests((context, builder) -> CommandSource.suggestMatching(CARPET_ACTION_HINTS, builder)) + .executes(context -> forwardAction( + context.getSource(), + IntegerArgumentType.getInteger(context, "id"), + StringArgumentType.getString(context, "action") + )))) + ); + } + + private static int showConfig(ServerCommandSource source) { + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + source.sendFeedback(() -> Text.literal("cbot config:"), false); + source.sendFeedback(() -> Text.literal("botname: " + state.getBotNamePattern()), false); + source.sendFeedback(() -> Text.literal("starting-free: " + state.getStartingFreeBots()), false); + source.sendFeedback(() -> Text.literal("price: " + state.getPriceItemId() + " x" + state.getPriceCount()), false); + source.sendFeedback(() -> Text.literal("cap: " + state.getBotCapDisplay()), false); + return 1; + } + + private static int setBotNamePattern(ServerCommandSource source, String pattern) { + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + int renamed = state.setBotNamePatternAndRename(pattern); + source.sendFeedback(() -> Text.literal("Set botname pattern to: " + state.getBotNamePattern()), true); + source.sendFeedback( + () -> Text.literal("Renamed " + renamed + " saved bot identities. Already-spawned old-name bots may need to be killed or restarted manually."), + true + ); + return 1; + } + + private static int setStartingFreeBots(ServerCommandSource source, int count) { + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + state.setStartingFreeBots(count); + source.sendFeedback(() -> Text.literal("Set starting free bots to " + count), true); + return 1; + } + + private static int setPrice(ServerCommandSource source, Identifier identifier, int count) { + if (Registries.ITEM.getOptionalValue(identifier).isEmpty()) { + source.sendError(Text.literal("Unknown item: " + identifier)); + return 0; + } + + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + state.setPrice(identifier.toString(), count); + source.sendFeedback(() -> Text.literal("Set bot price to " + identifier + " x" + count), true); + return 1; + } + + private static int setBotCap(ServerCommandSource source, int cap) { + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + state.setBotCap(cap); + source.sendFeedback(() -> Text.literal("Set bot cap to " + state.getBotCapDisplay()), true); + return 1; + } + + private static int resetConfig(ServerCommandSource source) { + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + int renamed = state.resetConfigAndRename(); + source.sendFeedback(() -> Text.literal("Reset cbot config to defaults"), true); + source.sendFeedback( + () -> Text.literal("Renamed " + renamed + " saved bot identities. Already-spawned old-name bots may need to be killed or restarted manually."), + true + ); + return 1; + } + + private static int listBots(ServerCommandSource source) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + CbotPersistentState state = CbotPersistentState.get(source.getServer()); + state.ensureFreeBots(player.getUuid(), CbotService.playerName(player)); + + List bots = state.getBots(player.getUuid()); + if (bots.isEmpty()) { + source.sendFeedback(() -> Text.literal("You have no bots"), false); + return 1; + } + + source.sendFeedback(() -> Text.literal("Your bots:"), false); + for (int i = 0; i < bots.size(); i++) { + String botName = bots.get(i); + boolean online = source.getServer().getPlayerManager().getPlayer(botName) != null; + int id = i + 1; + source.sendFeedback( + () -> Text.literal("#" + id + " " + botName + " [" + (online ? "online" : "offline") + "]"), + false + ); + } + + return bots.size(); + } + + private static int buyBot(ServerCommandSource source) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + CbotOperationResult result = CbotService.buyBot(player); + CbotService.sendChatResult(source, result); + return result.success() ? 1 : 0; + } + + private static int openGui(ServerCommandSource source) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + if (!CbotNetworking.canOpenGui(player)) { + source.sendError(Text.literal("Install cbot on your client to use the GUI")); + return 0; + } + + ServerPlayNetworking.send(player, new OpenScreenS2CPayload(CbotService.buildState(player, ""))); + source.sendFeedback(() -> Text.literal("Opening cbot GUI"), false); + return 1; + } + + private static int spawnBot(ServerCommandSource source, int id) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + if (!isCarpetLoaded()) { + source.sendError(Text.literal("Carpet mod is not installed")); + return 0; + } + + CbotOperationResult result = CbotService.spawnBot(player, id); + CbotService.sendChatResult(source, result); + return result.success() ? 1 : 0; + } + + private static int tpToOwner(ServerCommandSource source, int id) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + CbotOperationResult result = CbotService.tpToOwner(player, id); + CbotService.sendChatResult(source, result); + return result.success() ? 1 : 0; + } + + private static int tpToCoords(ServerCommandSource source, int id, double x, double y, double z) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + CbotOperationResult result = CbotService.tpToCoords(player, id, x, y, z); + CbotService.sendChatResult(source, result); + return result.success() ? 1 : 0; + } + + private static int forwardAction(ServerCommandSource source, int id, String action) throws CommandSyntaxException { + ServerPlayerEntity player = requirePlayer(source); + if (!isCarpetLoaded()) { + source.sendError(Text.literal("Carpet mod is not installed")); + return 0; + } + + CbotOperationResult result = CbotService.forwardAction(player, id, action); + CbotService.sendChatResult(source, result); + return result.success() ? 1 : 0; + } + + private static ServerPlayerEntity requirePlayer(ServerCommandSource source) throws CommandSyntaxException { + ServerPlayerEntity player; + try { + player = source.getPlayerOrThrow(); + } catch (CommandSyntaxException exception) { + throw PLAYER_ONLY.create(); + } + + if (isFakePlayer(player)) { + throw REAL_PLAYER_ONLY.create(); + } + return player; + } + + private static boolean isCarpetLoaded() { + return FabricLoader.getInstance().isModLoaded(CARPET_MOD_ID); + } + + private static boolean isFakePlayer(ServerPlayerEntity player) { + String className = player.getClass().getName().toLowerCase(); + if (className.contains("fake")) { + return true; + } + + if (!isCarpetLoaded()) { + return false; + } + + try { + Class carpetFakeClass = Class.forName("carpet.patches.EntityPlayerMPFake"); + return carpetFakeClass.isInstance(player); + } catch (ClassNotFoundException ignored) { + return false; + } + } +} diff --git a/src/main/java/com/feror/cbot/network/ActionC2SPayload.java b/src/main/java/com/feror/cbot/network/ActionC2SPayload.java new file mode 100644 index 0000000..fb65708 --- /dev/null +++ b/src/main/java/com/feror/cbot/network/ActionC2SPayload.java @@ -0,0 +1,63 @@ +package com.feror.cbot.network; + +import com.feror.cbot.CbotMod; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public record ActionC2SPayload(Action action, int botId, double x, double y, double z, String carpetAction) implements CustomPayload { + public static final Id ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "action")); + public static final PacketCodec CODEC = PacketCodec.ofStatic( + (buf, payload) -> { + buf.writeEnumConstant(payload.action()); + buf.writeInt(payload.botId()); + buf.writeDouble(payload.x()); + buf.writeDouble(payload.y()); + buf.writeDouble(payload.z()); + buf.writeString(payload.carpetAction() == null ? "" : payload.carpetAction()); + }, + buf -> new ActionC2SPayload( + buf.readEnumConstant(Action.class), + buf.readInt(), + buf.readDouble(), + buf.readDouble(), + buf.readDouble(), + buf.readString() + ) + ); + + public static ActionC2SPayload refresh() { + return new ActionC2SPayload(Action.REFRESH, 0, 0, 0, 0, ""); + } + + public static ActionC2SPayload buy() { + return new ActionC2SPayload(Action.BUY, 0, 0, 0, 0, ""); + } + + public static ActionC2SPayload bot(Action action, int botId) { + return new ActionC2SPayload(action, botId, 0, 0, 0, ""); + } + + public static ActionC2SPayload tpToCoords(int botId, double x, double y, double z) { + return new ActionC2SPayload(Action.TP_TO_COORDS, botId, x, y, z, ""); + } + + public static ActionC2SPayload forward(int botId, String carpetAction) { + return new ActionC2SPayload(Action.FORWARD_ACTION, botId, 0, 0, 0, carpetAction); + } + + @Override + public Id getId() { + return ID; + } + + public enum Action { + BUY, + SPAWN, + TP_TO_SELF, + TP_TO_COORDS, + FORWARD_ACTION, + REFRESH + } +} diff --git a/src/main/java/com/feror/cbot/network/ActionResultS2CPayload.java b/src/main/java/com/feror/cbot/network/ActionResultS2CPayload.java new file mode 100644 index 0000000..4b3c401 --- /dev/null +++ b/src/main/java/com/feror/cbot/network/ActionResultS2CPayload.java @@ -0,0 +1,23 @@ +package com.feror.cbot.network; + +import com.feror.cbot.CbotMod; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public record ActionResultS2CPayload(boolean success, CbotStateData state) implements CustomPayload { + public static final Id ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "action_result")); + public static final PacketCodec CODEC = PacketCodec.ofStatic( + (buf, payload) -> { + buf.writeBoolean(payload.success()); + payload.state().write(buf); + }, + buf -> new ActionResultS2CPayload(buf.readBoolean(), CbotStateData.read(buf)) + ); + + @Override + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/feror/cbot/network/CbotNetworking.java b/src/main/java/com/feror/cbot/network/CbotNetworking.java new file mode 100644 index 0000000..9148a5e --- /dev/null +++ b/src/main/java/com/feror/cbot/network/CbotNetworking.java @@ -0,0 +1,45 @@ +package com.feror.cbot.network; + +import com.feror.cbot.service.CbotOperationResult; +import com.feror.cbot.service.CbotService; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.network.ServerPlayerEntity; + +public final class CbotNetworking { + private CbotNetworking() {} + + public static void registerPayloads() { + PayloadTypeRegistry.playC2S().register(RequestStateC2SPayload.ID, RequestStateC2SPayload.CODEC); + PayloadTypeRegistry.playC2S().register(ActionC2SPayload.ID, ActionC2SPayload.CODEC); + PayloadTypeRegistry.playS2C().register(OpenScreenS2CPayload.ID, OpenScreenS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(StateS2CPayload.ID, StateS2CPayload.CODEC); + PayloadTypeRegistry.playS2C().register(ActionResultS2CPayload.ID, ActionResultS2CPayload.CODEC); + } + + public static void registerServerReceivers() { + ServerPlayNetworking.registerGlobalReceiver(RequestStateC2SPayload.ID, (payload, context) -> { + ServerPlayerEntity player = context.player(); + ServerPlayNetworking.send(player, new OpenScreenS2CPayload(CbotService.buildState(player, ""))); + }); + + ServerPlayNetworking.registerGlobalReceiver(ActionC2SPayload.ID, (payload, context) -> { + ServerPlayerEntity player = context.player(); + CbotOperationResult result = switch (payload.action()) { + case BUY -> CbotService.buyBot(player); + case SPAWN -> CbotService.spawnBot(player, payload.botId()); + case TP_TO_SELF -> CbotService.tpToOwner(player, payload.botId()); + case TP_TO_COORDS -> CbotService.tpToCoords(player, payload.botId(), payload.x(), payload.y(), payload.z()); + case FORWARD_ACTION -> CbotService.forwardAction(player, payload.botId(), payload.carpetAction()); + case REFRESH -> CbotService.refresh(player); + }; + ServerPlayNetworking.send(player, new ActionResultS2CPayload(result.success(), result.state())); + }); + } + + public static boolean canOpenGui(ServerPlayerEntity player) { + return ServerPlayNetworking.canSend(player, OpenScreenS2CPayload.ID) + && ServerPlayNetworking.canSend(player, StateS2CPayload.ID) + && ServerPlayNetworking.canSend(player, ActionResultS2CPayload.ID); + } +} diff --git a/src/main/java/com/feror/cbot/network/CbotStateData.java b/src/main/java/com/feror/cbot/network/CbotStateData.java new file mode 100644 index 0000000..465b2e0 --- /dev/null +++ b/src/main/java/com/feror/cbot/network/CbotStateData.java @@ -0,0 +1,73 @@ +package com.feror.cbot.network; + +import net.minecraft.network.PacketByteBuf; + +import java.util.ArrayList; +import java.util.List; + +public record CbotStateData( + List bots, + String priceItemId, + int priceCount, + String capDisplay, + int startingFreeBots, + boolean carpetLoaded, + boolean hasCurrency, + boolean canBuy, + boolean capReached, + String message +) { + public CbotStateData { + bots = List.copyOf(bots); + message = message == null ? "" : message; + } + + public void write(PacketByteBuf buf) { + buf.writeCollection(bots, (botBuf, bot) -> bot.write(botBuf)); + buf.writeString(priceItemId); + buf.writeInt(priceCount); + buf.writeString(capDisplay); + buf.writeInt(startingFreeBots); + buf.writeBoolean(carpetLoaded); + buf.writeBoolean(hasCurrency); + buf.writeBoolean(canBuy); + buf.writeBoolean(capReached); + buf.writeString(message); + } + + public static CbotStateData read(PacketByteBuf buf) { + List bots = buf.readList(BotData::read); + return new CbotStateData( + bots, + buf.readString(), + buf.readInt(), + buf.readString(), + buf.readInt(), + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readString() + ); + } + + public CbotStateData withMessage(String message) { + return new CbotStateData(bots, priceItemId, priceCount, capDisplay, startingFreeBots, carpetLoaded, hasCurrency, canBuy, capReached, message); + } + + public record BotData(int id, String name, boolean online) { + private void write(PacketByteBuf buf) { + buf.writeInt(id); + buf.writeString(name); + buf.writeBoolean(online); + } + + private static BotData read(PacketByteBuf buf) { + return new BotData(buf.readInt(), buf.readString(), buf.readBoolean()); + } + } + + public static CbotStateData empty(String message) { + return new CbotStateData(new ArrayList<>(), "minecraft:netherite_block", 1, "none", 1, false, false, false, false, message); + } +} diff --git a/src/main/java/com/feror/cbot/network/OpenScreenS2CPayload.java b/src/main/java/com/feror/cbot/network/OpenScreenS2CPayload.java new file mode 100644 index 0000000..29cc225 --- /dev/null +++ b/src/main/java/com/feror/cbot/network/OpenScreenS2CPayload.java @@ -0,0 +1,20 @@ +package com.feror.cbot.network; + +import com.feror.cbot.CbotMod; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public record OpenScreenS2CPayload(CbotStateData state) implements CustomPayload { + public static final Id ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "open_screen")); + public static final PacketCodec CODEC = PacketCodec.ofStatic( + (buf, payload) -> payload.state().write(buf), + buf -> new OpenScreenS2CPayload(CbotStateData.read(buf)) + ); + + @Override + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/feror/cbot/network/RequestStateC2SPayload.java b/src/main/java/com/feror/cbot/network/RequestStateC2SPayload.java new file mode 100644 index 0000000..cef1c59 --- /dev/null +++ b/src/main/java/com/feror/cbot/network/RequestStateC2SPayload.java @@ -0,0 +1,17 @@ +package com.feror.cbot.network; + +import com.feror.cbot.CbotMod; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public record RequestStateC2SPayload() implements CustomPayload { + public static final Id ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "request_state")); + public static final PacketCodec CODEC = PacketCodec.unit(new RequestStateC2SPayload()); + + @Override + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/feror/cbot/network/StateS2CPayload.java b/src/main/java/com/feror/cbot/network/StateS2CPayload.java new file mode 100644 index 0000000..b35362a --- /dev/null +++ b/src/main/java/com/feror/cbot/network/StateS2CPayload.java @@ -0,0 +1,20 @@ +package com.feror.cbot.network; + +import com.feror.cbot.CbotMod; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; + +public record StateS2CPayload(CbotStateData state) implements CustomPayload { + public static final Id ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "state")); + public static final PacketCodec CODEC = PacketCodec.ofStatic( + (buf, payload) -> payload.state().write(buf), + buf -> new StateS2CPayload(CbotStateData.read(buf)) + ); + + @Override + public Id getId() { + return ID; + } +} diff --git a/src/main/java/com/feror/cbot/service/CbotOperationResult.java b/src/main/java/com/feror/cbot/service/CbotOperationResult.java new file mode 100644 index 0000000..5b1f2d9 --- /dev/null +++ b/src/main/java/com/feror/cbot/service/CbotOperationResult.java @@ -0,0 +1,5 @@ +package com.feror.cbot.service; + +import com.feror.cbot.network.CbotStateData; + +public record CbotOperationResult(boolean success, String message, CbotStateData state) {} diff --git a/src/main/java/com/feror/cbot/service/CbotService.java b/src/main/java/com/feror/cbot/service/CbotService.java new file mode 100644 index 0000000..53245c1 --- /dev/null +++ b/src/main/java/com/feror/cbot/service/CbotService.java @@ -0,0 +1,192 @@ +package com.feror.cbot.service; + +import com.feror.cbot.CbotMod; +import com.feror.cbot.network.CbotStateData; +import com.feror.cbot.state.CbotPersistentState; +import com.feror.cbot.util.CommandDispatchUtil; +import com.feror.cbot.util.InventoryUtil; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.item.Item; +import net.minecraft.registry.Registries; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +import java.util.ArrayList; +import java.util.List; + +public final class CbotService { + private static final String CARPET_MOD_ID = "carpet"; + + private CbotService() {} + + public static CbotStateData buildState(ServerPlayerEntity player, String message) { + CbotPersistentState state = CbotPersistentState.get(player.getCommandSource().getServer()); + state.ensureFreeBots(player.getUuid(), playerName(player)); + + List bots = new ArrayList<>(); + List botNames = state.getBots(player.getUuid()); + for (int i = 0; i < botNames.size(); i++) { + String botName = botNames.get(i); + boolean online = player.getCommandSource().getServer().getPlayerManager().getPlayer(botName) != null; + bots.add(new CbotStateData.BotData(i + 1, botName, online)); + } + + boolean capAllowsBuy = state.canBuyBot(player.getUuid()); + Item priceItem = resolveItem(state.getPriceItemId()); + boolean hasCurrency = state.getPriceCount() <= 0 + || (priceItem != null && InventoryUtil.count(player, priceItem) >= state.getPriceCount()); + boolean canBuy = capAllowsBuy && hasCurrency; + return new CbotStateData( + bots, + state.getPriceItemId(), + state.getPriceCount(), + state.getBotCapDisplay(), + state.getStartingFreeBots(), + isCarpetLoaded(), + hasCurrency, + canBuy, + !capAllowsBuy, + message == null ? "" : message + ); + } + + public static CbotOperationResult buyBot(ServerPlayerEntity player) { + CbotPersistentState state = CbotPersistentState.get(player.getCommandSource().getServer()); + state.ensureFreeBots(player.getUuid(), playerName(player)); + + if (!state.canBuyBot(player.getUuid())) { + return result(player, false, "You have reached the bot cap"); + } + + Item priceItem = resolveItem(state.getPriceItemId()); + if (priceItem == null) { + return result(player, false, "Configured bot price item is invalid: " + state.getPriceItemId()); + } + + int priceCount = state.getPriceCount(); + if (!InventoryUtil.consume(player, priceItem, priceCount)) { + return result(player, false, "You need " + priceCount + " " + state.getPriceItemId()); + } + + CbotPersistentState.OwnedBot newBot = state.buyBot(player.getUuid(), playerName(player)); + return result(player, true, "Bought bot #" + newBot.id() + " (" + newBot.name() + ")"); + } + + public static CbotOperationResult spawnBot(ServerPlayerEntity player, int id) { + if (!isCarpetLoaded()) { + return result(player, false, "Carpet mod is not installed"); + } + + String botName = resolveOwnedBot(player, id); + if (botName == null) { + return result(player, false, "Invalid bot id"); + } + + String command = "player " + botName + " spawn"; + logAction(player, botName, "spawn", command); + int commandResult = CommandDispatchUtil.executeAsServer(player.getCommandSource(), command); + if (commandResult <= 0) { + return result(player, false, "Failed to spawn " + botName); + } + return result(player, true, "Spawned " + botName); + } + + public static CbotOperationResult tpToOwner(ServerPlayerEntity player, int id) { + String botName = resolveOwnedBot(player, id); + if (botName == null) { + return result(player, false, "Invalid bot id"); + } + + String command = "tp " + botName + " " + playerName(player); + logAction(player, botName, "tp_to_player", command); + int commandResult = CommandDispatchUtil.executeAsServer(player.getCommandSource(), command); + if (commandResult <= 0) { + return result(player, false, "Failed to teleport " + botName); + } + return result(player, true, "Teleported " + botName + " to you"); + } + + public static CbotOperationResult tpToCoords(ServerPlayerEntity player, int id, double x, double y, double z) { + String botName = resolveOwnedBot(player, id); + if (botName == null) { + return result(player, false, "Invalid bot id"); + } + + String command = "tp " + botName + " " + x + " " + y + " " + z; + logAction(player, botName, "tp_to_coords", command); + int commandResult = CommandDispatchUtil.executeAsServer(player.getCommandSource(), command); + if (commandResult <= 0) { + return result(player, false, "Failed to teleport " + botName); + } + return result(player, true, "Teleported " + botName); + } + + public static CbotOperationResult forwardAction(ServerPlayerEntity player, int id, String action) { + if (!isCarpetLoaded()) { + return result(player, false, "Carpet mod is not installed"); + } + + String botName = resolveOwnedBot(player, id); + if (botName == null) { + return result(player, false, "Invalid bot id"); + } + + String command = "player " + botName + " " + action; + logAction(player, botName, action, command); + int commandResult = CommandDispatchUtil.executeAsServer(player.getCommandSource(), command); + if (commandResult <= 0) { + return result(player, false, "Failed to run action on " + botName); + } + return result(player, true, "Sent action to " + botName); + } + + public static CbotOperationResult refresh(ServerPlayerEntity player) { + return result(player, true, "Refreshed"); + } + + public static void sendChatResult(ServerCommandSource source, CbotOperationResult result) { + if (result.success()) { + source.sendFeedback(() -> net.minecraft.text.Text.literal(result.message()), false); + } else { + source.sendError(net.minecraft.text.Text.literal(result.message())); + } + } + + private static CbotOperationResult result(ServerPlayerEntity player, boolean success, String message) { + return new CbotOperationResult(success, message, buildState(player, message)); + } + + private static String resolveOwnedBot(ServerPlayerEntity player, int id) { + CbotPersistentState state = CbotPersistentState.get(player.getCommandSource().getServer()); + state.ensureFreeBots(player.getUuid(), playerName(player)); + return state.getBotName(player.getUuid(), id); + } + + private static Item resolveItem(String itemId) { + Identifier identifier = Identifier.tryParse(itemId); + if (identifier == null) { + return null; + } + return Registries.ITEM.getOptionalValue(identifier).orElse(null); + } + + public static boolean isCarpetLoaded() { + return FabricLoader.getInstance().isModLoaded(CARPET_MOD_ID); + } + + public static String playerName(ServerPlayerEntity player) { + return player.getStringifiedName(); + } + + private static void logAction(ServerPlayerEntity owner, String botName, String action, String dispatchedCommand) { + CbotMod.LOGGER.info( + "cbot dispatch: owner={} ({}) bot={} action='{}' cmd='{}'", + playerName(owner), + owner.getUuid(), + botName, + action, + dispatchedCommand + ); + } +} diff --git a/src/main/java/com/feror/cbot/state/CbotPersistentState.java b/src/main/java/com/feror/cbot/state/CbotPersistentState.java new file mode 100644 index 0000000..99f4e24 --- /dev/null +++ b/src/main/java/com/feror/cbot/state/CbotPersistentState.java @@ -0,0 +1,365 @@ +package com.feror.cbot.state; + +import com.feror.cbot.CbotMod; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.datafixer.DataFixTypes; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.PersistentState; +import net.minecraft.world.PersistentStateManager; +import net.minecraft.world.PersistentStateType; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CbotPersistentState extends PersistentState { + public static final String DEFAULT_BOT_NAME_PATTERN = "cbot_{player}_{id}"; + public static final int DEFAULT_STARTING_FREE_BOTS = 1; + public static final String DEFAULT_PRICE_ITEM_ID = "minecraft:netherite_block"; + public static final int DEFAULT_PRICE_COUNT = 1; + public static final int UNLIMITED_BOT_CAP = -1; + + private static final String DATA_NAME = CbotMod.MOD_ID + "_state"; + private static final int MAX_BOT_NAME_LENGTH = 16; + private static final Pattern UNSAFE_NAME_CHARS = Pattern.compile("[^a-zA-Z0-9_]"); + private static final Pattern OLD_DEFAULT_NAME = Pattern.compile("^cbot_(.+)_\\d+$"); + + private static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + OwnerData.CODEC.listOf().optionalFieldOf("owners", List.of()).forGetter(CbotPersistentState::toOwnerData), + ConfigData.CODEC.optionalFieldOf("config", ConfigData.DEFAULT).forGetter(CbotPersistentState::toConfigData) + ).apply(instance, CbotPersistentState::fromCodecData) + ); + + public static final PersistentStateType TYPE = new PersistentStateType<>( + DATA_NAME, + CbotPersistentState::new, + CODEC, + DataFixTypes.LEVEL + ); + + private final Map> ownedBots = new HashMap<>(); + private final Map ownerNames = new HashMap<>(); + + private String botNamePattern = DEFAULT_BOT_NAME_PATTERN; + private int startingFreeBots = DEFAULT_STARTING_FREE_BOTS; + private String priceItemId = DEFAULT_PRICE_ITEM_ID; + private int priceCount = DEFAULT_PRICE_COUNT; + private int botCap = UNLIMITED_BOT_CAP; + + public static CbotPersistentState get(MinecraftServer server) { + PersistentStateManager manager = server.getOverworld().getPersistentStateManager(); + return manager.getOrCreate(TYPE); + } + + private static CbotPersistentState fromCodecData(List owners, ConfigData config) { + CbotPersistentState state = new CbotPersistentState(); + state.botNamePattern = config.botNamePattern(); + state.startingFreeBots = Math.max(0, config.startingFreeBots()); + state.priceItemId = config.priceItemId(); + state.priceCount = Math.max(0, config.priceCount()); + state.botCap = config.botCap() < 0 ? UNLIMITED_BOT_CAP : config.botCap(); + + for (OwnerData owner : owners) { + UUID uuid; + try { + uuid = UUID.fromString(owner.uuid()); + } catch (IllegalArgumentException ignored) { + continue; + } + + if (!owner.bots().isEmpty()) { + state.ownedBots.put(uuid, new ArrayList<>(owner.bots())); + } + + String ownerName = owner.ownerName(); + if (ownerName.isBlank()) { + ownerName = inferOwnerName(owner.bots()); + } + if (!ownerName.isBlank()) { + state.ownerNames.put(uuid, ownerName); + } + } + + return state; + } + + private List toOwnerData() { + List owners = new ArrayList<>(); + + for (Map.Entry> entry : ownedBots.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + + String ownerName = ownerNames.getOrDefault(entry.getKey(), inferOwnerName(entry.getValue())); + owners.add(new OwnerData(entry.getKey().toString(), ownerName, List.copyOf(entry.getValue()))); + } + + return owners; + } + + private ConfigData toConfigData() { + return new ConfigData(botNamePattern, startingFreeBots, priceItemId, priceCount, botCap); + } + + public List getBots(UUID ownerUuid) { + List bots = ownedBots.get(ownerUuid); + if (bots == null) { + return List.of(); + } + return List.copyOf(bots); + } + + public String getBotName(UUID ownerUuid, int id) { + if (id <= 0) { + return null; + } + List bots = ownedBots.get(ownerUuid); + if (bots == null || id > bots.size()) { + return null; + } + return bots.get(id - 1); + } + + public void ensureFreeBots(UUID ownerUuid, String ownerName) { + rememberOwnerName(ownerUuid, ownerName); + List bots = ownedBots.computeIfAbsent(ownerUuid, ignored -> new ArrayList<>()); + int desiredCount = cappedFreeBotCount(); + if (bots.size() >= desiredCount) { + return; + } + + Set reservedNames = allBotNames(); + for (int id = bots.size() + 1; id <= desiredCount; id++) { + String botName = buildUniqueBotName(ownerUuid, ownerName, id, reservedNames); + bots.add(botName); + CbotMod.LOGGER.info("Created free bot for {} ({}) -> {}", ownerName, ownerUuid, botName); + } + markDirty(); + } + + public OwnedBot buyBot(UUID ownerUuid, String ownerName) { + rememberOwnerName(ownerUuid, ownerName); + List bots = ownedBots.computeIfAbsent(ownerUuid, ignored -> new ArrayList<>()); + int nextId = bots.size() + 1; + String botName = buildUniqueBotName(ownerUuid, ownerName, nextId, allBotNames()); + bots.add(botName); + markDirty(); + return new OwnedBot(nextId, botName); + } + + public boolean canBuyBot(UUID ownerUuid) { + if (botCap == UNLIMITED_BOT_CAP) { + return true; + } + return getBots(ownerUuid).size() < botCap; + } + + public int getBotCap() { + return botCap; + } + + public String getBotCapDisplay() { + return botCap == UNLIMITED_BOT_CAP ? "none" : Integer.toString(botCap); + } + + public String getBotNamePattern() { + return botNamePattern; + } + + public int getStartingFreeBots() { + return startingFreeBots; + } + + public String getPriceItemId() { + return priceItemId; + } + + public int getPriceCount() { + return priceCount; + } + + public void setStartingFreeBots(int startingFreeBots) { + this.startingFreeBots = Math.max(0, startingFreeBots); + markDirty(); + } + + public void setPrice(String priceItemId, int priceCount) { + this.priceItemId = priceItemId; + this.priceCount = Math.max(0, priceCount); + markDirty(); + } + + public void setBotCap(int botCap) { + this.botCap = botCap < 0 ? UNLIMITED_BOT_CAP : botCap; + markDirty(); + } + + public int setBotNamePatternAndRename(String botNamePattern) { + this.botNamePattern = botNamePattern.isBlank() ? DEFAULT_BOT_NAME_PATTERN : botNamePattern; + int renamed = renameAllBots(); + markDirty(); + return renamed; + } + + public int resetConfigAndRename() { + botNamePattern = DEFAULT_BOT_NAME_PATTERN; + startingFreeBots = DEFAULT_STARTING_FREE_BOTS; + priceItemId = DEFAULT_PRICE_ITEM_ID; + priceCount = DEFAULT_PRICE_COUNT; + botCap = UNLIMITED_BOT_CAP; + int renamed = renameAllBots(); + markDirty(); + return renamed; + } + + private int renameAllBots() { + Set reservedNames = new HashSet<>(); + int renamed = 0; + List owners = ownedBots.keySet().stream() + .sorted(Comparator.comparing(UUID::toString)) + .toList(); + + for (UUID ownerUuid : owners) { + List oldBots = ownedBots.get(ownerUuid); + if (oldBots == null || oldBots.isEmpty()) { + continue; + } + + String ownerName = ownerNames.getOrDefault(ownerUuid, inferOwnerName(oldBots)); + if (ownerName.isBlank()) { + ownerName = ownerUuid.toString().substring(0, 8); + } + ownerNames.put(ownerUuid, ownerName); + + List newBots = new ArrayList<>(); + for (int index = 0; index < oldBots.size(); index++) { + String newName = buildUniqueBotName(ownerUuid, ownerName, index + 1, reservedNames); + if (!newName.equals(oldBots.get(index))) { + renamed++; + } + newBots.add(newName); + } + ownedBots.put(ownerUuid, newBots); + } + + return renamed; + } + + private void rememberOwnerName(UUID ownerUuid, String ownerName) { + if (!ownerName.isBlank() && !ownerName.equals(ownerNames.get(ownerUuid))) { + ownerNames.put(ownerUuid, ownerName); + markDirty(); + } + } + + private int cappedFreeBotCount() { + if (botCap == UNLIMITED_BOT_CAP) { + return startingFreeBots; + } + return Math.min(startingFreeBots, botCap); + } + + private Set allBotNames() { + Set names = new HashSet<>(); + for (List bots : ownedBots.values()) { + names.addAll(bots); + } + return names; + } + + private String buildUniqueBotName(UUID ownerUuid, String ownerName, int id, Set reservedNames) { + String rendered = renderBotName(ownerUuid, ownerName, id); + String name = fitBotName(rendered, ownerUuid, id, 0); + int salt = 1; + while (reservedNames.contains(name)) { + name = fitBotName(rendered, ownerUuid, id, salt++); + } + reservedNames.add(name); + return name; + } + + private String renderBotName(UUID ownerUuid, String ownerName, int id) { + String rendered = botNamePattern + .replace("{player}", ownerName) + .replace("{id}", Integer.toString(id)) + .replace("{uuid}", ownerUuid.toString()); + rendered = UNSAFE_NAME_CHARS.matcher(rendered).replaceAll("_"); + if (rendered.isBlank()) { + rendered = "cbot" + id; + } + return rendered; + } + + private static String fitBotName(String rendered, UUID ownerUuid, int id, int salt) { + if (rendered.length() <= MAX_BOT_NAME_LENGTH && salt == 0) { + return rendered; + } + + String hash = shortHash(ownerUuid, id, salt, rendered); + int prefixLength = MAX_BOT_NAME_LENGTH - hash.length() - 1; + String prefix = rendered.length() > prefixLength ? rendered.substring(0, prefixLength) : rendered; + return prefix + "_" + hash; + } + + private static String shortHash(UUID ownerUuid, int id, int salt, String rendered) { + String hash = Integer.toUnsignedString(Objects.hash(ownerUuid, id, salt, rendered), 36); + if (hash.length() >= 5) { + return hash.substring(0, 5); + } + return ("00000" + hash).substring(hash.length()); + } + + private static String inferOwnerName(List bots) { + if (bots.isEmpty()) { + return ""; + } + Matcher matcher = OLD_DEFAULT_NAME.matcher(bots.getFirst()); + if (matcher.matches()) { + return matcher.group(1); + } + return ""; + } + + public record OwnedBot(int id, String name) {} + + private record OwnerData(String uuid, String ownerName, List bots) { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.STRING.fieldOf("uuid").forGetter(OwnerData::uuid), + Codec.STRING.optionalFieldOf("ownerName", "").forGetter(OwnerData::ownerName), + Codec.STRING.listOf().fieldOf("bots").forGetter(OwnerData::bots) + ).apply(instance, OwnerData::new) + ); + } + + private record ConfigData(String botNamePattern, int startingFreeBots, String priceItemId, int priceCount, int botCap) { + private static final ConfigData DEFAULT = new ConfigData( + DEFAULT_BOT_NAME_PATTERN, + DEFAULT_STARTING_FREE_BOTS, + DEFAULT_PRICE_ITEM_ID, + DEFAULT_PRICE_COUNT, + UNLIMITED_BOT_CAP + ); + + private static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + Codec.STRING.optionalFieldOf("botNamePattern", DEFAULT_BOT_NAME_PATTERN).forGetter(ConfigData::botNamePattern), + Codec.INT.optionalFieldOf("startingFreeBots", DEFAULT_STARTING_FREE_BOTS).forGetter(ConfigData::startingFreeBots), + Codec.STRING.optionalFieldOf("priceItem", DEFAULT_PRICE_ITEM_ID).forGetter(ConfigData::priceItemId), + Codec.INT.optionalFieldOf("priceCount", DEFAULT_PRICE_COUNT).forGetter(ConfigData::priceCount), + Codec.INT.optionalFieldOf("botCap", UNLIMITED_BOT_CAP).forGetter(ConfigData::botCap) + ).apply(instance, ConfigData::new) + ); + } +} diff --git a/src/main/java/com/feror/cbot/util/CommandDispatchUtil.java b/src/main/java/com/feror/cbot/util/CommandDispatchUtil.java new file mode 100644 index 0000000..73d8467 --- /dev/null +++ b/src/main/java/com/feror/cbot/util/CommandDispatchUtil.java @@ -0,0 +1,18 @@ +package com.feror.cbot.util; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.command.permission.PermissionPredicate; +import net.minecraft.server.command.ServerCommandSource; + +public final class CommandDispatchUtil { + private CommandDispatchUtil() {} + + public static int executeAsServer(ServerCommandSource baseSource, String command) { + ServerCommandSource elevatedSource = baseSource.withPermissions(PermissionPredicate.ALL); + try { + return baseSource.getServer().getCommandManager().getDispatcher().execute(command, elevatedSource); + } catch (CommandSyntaxException exception) { + return 0; + } + } +} diff --git a/src/main/java/com/feror/cbot/util/InventoryUtil.java b/src/main/java/com/feror/cbot/util/InventoryUtil.java new file mode 100644 index 0000000..f1246bc --- /dev/null +++ b/src/main/java/com/feror/cbot/util/InventoryUtil.java @@ -0,0 +1,60 @@ +package com.feror.cbot.util; + +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; + +public final class InventoryUtil { + private InventoryUtil() {} + + public static boolean consumeOne(ServerPlayerEntity player, Item item) { + return consume(player, item, 1); + } + + public static boolean consume(ServerPlayerEntity player, Item item, int count) { + if (count <= 0) { + return true; + } + if (count(player, item) < count) { + return false; + } + + PlayerInventory inventory = player.getInventory(); + int remaining = count; + + for (int i = 0; i < inventory.size(); i++) { + ItemStack stack = inventory.getStack(i); + if (!stack.isOf(item) || stack.isEmpty()) { + continue; + } + + int consumed = Math.min(remaining, stack.getCount()); + stack.decrement(consumed); + remaining -= consumed; + if (remaining == 0) { + inventory.markDirty(); + player.currentScreenHandler.sendContentUpdates(); + return true; + } + } + + return false; + } + + public static int count(ServerPlayerEntity player, Item item) { + PlayerInventory inventory = player.getInventory(); + int total = 0; + + for (int i = 0; i < inventory.size(); i++) { + ItemStack stack = inventory.getStack(i); + if (!stack.isOf(item) || stack.isEmpty()) { + continue; + } + + total += stack.getCount(); + } + + return total; + } +} diff --git a/src/main/resources/assets/cbot/lang/en_us.json b/src/main/resources/assets/cbot/lang/en_us.json new file mode 100644 index 0000000..a81969c --- /dev/null +++ b/src/main/resources/assets/cbot/lang/en_us.json @@ -0,0 +1,4 @@ +{ + "key.categories.cbot": "Carpet Bot Manager", + "key.cbot.open_gui": "Open Carpet Bot Manager" +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..d609b56 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "id": "cbot", + "version": "${version}", + "name": "Carpet Bot Manager", + "description": "Player-owned bot management layer on top of Fabric Carpet /player", + "authors": [ + "Feror" + ], + "contact": {}, + "license": "MIT", + "environment": "*", + "entrypoints": { + "main": [ + "com.feror.cbot.CbotMod" + ], + "client": [ + "com.feror.cbot.client.CbotClientMod" + ] + }, + "depends": { + "fabricloader": ">=0.16.0", + "fabric-api": "*", + "minecraft": ">=1.21.11" + }, + "suggests": { + "carpet": "*" + } +}