CBOT V1.3
This commit is contained in:
commit
8e7ec034f2
31 changed files with 3524 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.gradle/
|
||||
build/
|
||||
out/
|
||||
run/
|
||||
*.iml
|
||||
.idea/
|
||||
*.class
|
||||
.DS_Store
|
||||
node_modules/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -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.
|
||||
123
README.md
Normal file
123
README.md
Normal file
|
|
@ -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 <ownedBotName> ...` or `tp <ownedBotName> ...` 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 <id> spawn`
|
||||
- Runs `player <botName> spawn` as server source.
|
||||
- `/cbot <id> tp`
|
||||
- Runs `tp <botName> <playerName>`.
|
||||
- `/cbot <id> tp <x> <y> <z>`
|
||||
- Runs `tp <botName> <x> <y> <z>`.
|
||||
- `/cbot <id> <carpetAction...>`
|
||||
- Forwards to `player <botName> <carpetAction...>`.
|
||||
- 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 "<pattern>"`
|
||||
- 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 <count>`
|
||||
- Sets how many free bots players are topped up to on next join or `/cbot` use.
|
||||
- `/cbot config price <item> <count>`
|
||||
- Sets the buy price, for example `minecraft:diamond 2`.
|
||||
- A count of `0` makes buying free.
|
||||
- `/cbot config cap <count>`
|
||||
- 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<UUID, List<String>> botNames`.
|
||||
- By default, a player gets exactly one starter bot:
|
||||
- `cbot_<ownerName>_1`.
|
||||
- Additional bots are deterministic and per-owner:
|
||||
- `cbot_<ownerName>_<n>`.
|
||||
- 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 <resolvedBotName> ...`
|
||||
- `tp <resolvedBotName> ...`
|
||||
- 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.
|
||||
53
build.gradle
Normal file
53
build.gradle
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
12
gradle.properties
Normal file
12
gradle.properties
Normal file
|
|
@ -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
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -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
|
||||
248
gradlew
vendored
Executable file
248
gradlew
vendored
Executable file
|
|
@ -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" "$@"
|
||||
93
gradlew.bat
vendored
Normal file
93
gradlew.bat
vendored
Normal file
|
|
@ -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
|
||||
954
package-lock.json
generated
Normal file
954
package-lock.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
388
scripts/e2e-cbot.mjs
Normal file
388
scripts/e2e-cbot.mjs
Normal file
|
|
@ -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);
|
||||
});
|
||||
12
settings.gradle
Normal file
12
settings.gradle
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
maven {
|
||||
name = "Fabric"
|
||||
url = "https://maven.fabricmc.net/"
|
||||
}
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "cbot"
|
||||
44
src/main/java/com/feror/cbot/CbotMod.java
Normal file
44
src/main/java/com/feror/cbot/CbotMod.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/main/java/com/feror/cbot/client/CbotClientMod.java
Normal file
62
src/main/java/com/feror/cbot/client/CbotClientMod.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
256
src/main/java/com/feror/cbot/client/CbotScreen.java
Normal file
256
src/main/java/com/feror/cbot/client/CbotScreen.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
296
src/main/java/com/feror/cbot/command/CbotCommand.java
Normal file
296
src/main/java/com/feror/cbot/command/CbotCommand.java
Normal file
|
|
@ -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<ServerCommandSource> 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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/main/java/com/feror/cbot/network/ActionC2SPayload.java
Normal file
63
src/main/java/com/feror/cbot/network/ActionC2SPayload.java
Normal file
|
|
@ -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<ActionC2SPayload> ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "action"));
|
||||
public static final PacketCodec<RegistryByteBuf, ActionC2SPayload> 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<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
BUY,
|
||||
SPAWN,
|
||||
TP_TO_SELF,
|
||||
TP_TO_COORDS,
|
||||
FORWARD_ACTION,
|
||||
REFRESH
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ActionResultS2CPayload> ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "action_result"));
|
||||
public static final PacketCodec<RegistryByteBuf, ActionResultS2CPayload> CODEC = PacketCodec.ofStatic(
|
||||
(buf, payload) -> {
|
||||
buf.writeBoolean(payload.success());
|
||||
payload.state().write(buf);
|
||||
},
|
||||
buf -> new ActionResultS2CPayload(buf.readBoolean(), CbotStateData.read(buf))
|
||||
);
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/feror/cbot/network/CbotNetworking.java
Normal file
45
src/main/java/com/feror/cbot/network/CbotNetworking.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
73
src/main/java/com/feror/cbot/network/CbotStateData.java
Normal file
73
src/main/java/com/feror/cbot/network/CbotStateData.java
Normal file
|
|
@ -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<BotData> 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<BotData> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OpenScreenS2CPayload> ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "open_screen"));
|
||||
public static final PacketCodec<RegistryByteBuf, OpenScreenS2CPayload> CODEC = PacketCodec.ofStatic(
|
||||
(buf, payload) -> payload.state().write(buf),
|
||||
buf -> new OpenScreenS2CPayload(CbotStateData.read(buf))
|
||||
);
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RequestStateC2SPayload> ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "request_state"));
|
||||
public static final PacketCodec<RegistryByteBuf, RequestStateC2SPayload> CODEC = PacketCodec.unit(new RequestStateC2SPayload());
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
20
src/main/java/com/feror/cbot/network/StateS2CPayload.java
Normal file
20
src/main/java/com/feror/cbot/network/StateS2CPayload.java
Normal file
|
|
@ -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<StateS2CPayload> ID = new Id<>(Identifier.of(CbotMod.MOD_ID, "state"));
|
||||
public static final PacketCodec<RegistryByteBuf, StateS2CPayload> CODEC = PacketCodec.ofStatic(
|
||||
(buf, payload) -> payload.state().write(buf),
|
||||
buf -> new StateS2CPayload(CbotStateData.read(buf))
|
||||
);
|
||||
|
||||
@Override
|
||||
public Id<? extends CustomPayload> getId() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.feror.cbot.service;
|
||||
|
||||
import com.feror.cbot.network.CbotStateData;
|
||||
|
||||
public record CbotOperationResult(boolean success, String message, CbotStateData state) {}
|
||||
192
src/main/java/com/feror/cbot/service/CbotService.java
Normal file
192
src/main/java/com/feror/cbot/service/CbotService.java
Normal file
|
|
@ -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<CbotStateData.BotData> bots = new ArrayList<>();
|
||||
List<String> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
365
src/main/java/com/feror/cbot/state/CbotPersistentState.java
Normal file
365
src/main/java/com/feror/cbot/state/CbotPersistentState.java
Normal file
|
|
@ -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<CbotPersistentState> 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<CbotPersistentState> TYPE = new PersistentStateType<>(
|
||||
DATA_NAME,
|
||||
CbotPersistentState::new,
|
||||
CODEC,
|
||||
DataFixTypes.LEVEL
|
||||
);
|
||||
|
||||
private final Map<UUID, List<String>> ownedBots = new HashMap<>();
|
||||
private final Map<UUID, String> 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<OwnerData> 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<OwnerData> toOwnerData() {
|
||||
List<OwnerData> owners = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<UUID, List<String>> 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<String> getBots(UUID ownerUuid) {
|
||||
List<String> 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<String> 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<String> bots = ownedBots.computeIfAbsent(ownerUuid, ignored -> new ArrayList<>());
|
||||
int desiredCount = cappedFreeBotCount();
|
||||
if (bots.size() >= desiredCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> 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<String> 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<String> reservedNames = new HashSet<>();
|
||||
int renamed = 0;
|
||||
List<UUID> owners = ownedBots.keySet().stream()
|
||||
.sorted(Comparator.comparing(UUID::toString))
|
||||
.toList();
|
||||
|
||||
for (UUID ownerUuid : owners) {
|
||||
List<String> 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<String> 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<String> allBotNames() {
|
||||
Set<String> names = new HashSet<>();
|
||||
for (List<String> bots : ownedBots.values()) {
|
||||
names.addAll(bots);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private String buildUniqueBotName(UUID ownerUuid, String ownerName, int id, Set<String> 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<String> 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<String> bots) {
|
||||
private static final Codec<OwnerData> 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<ConfigData> 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
18
src/main/java/com/feror/cbot/util/CommandDispatchUtil.java
Normal file
18
src/main/java/com/feror/cbot/util/CommandDispatchUtil.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/main/java/com/feror/cbot/util/InventoryUtil.java
Normal file
60
src/main/java/com/feror/cbot/util/InventoryUtil.java
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
4
src/main/resources/assets/cbot/lang/en_us.json
Normal file
4
src/main/resources/assets/cbot/lang/en_us.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"key.categories.cbot": "Carpet Bot Manager",
|
||||
"key.cbot.open_gui": "Open Carpet Bot Manager"
|
||||
}
|
||||
29
src/main/resources/fabric.mod.json
Normal file
29
src/main/resources/fabric.mod.json
Normal file
|
|
@ -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": "*"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue