diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 359bd388..8649102c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -21,13 +21,14 @@ jobs: - name: Check skip CI id: check-skip run: | - MSG="${{ github.event.head_commit.message }}" - LOWER=$(echo "$MSG" | tr '[:upper:]' '[:lower:]') + LOWER=$(echo "$COMMIT_MSG" | tr '[:upper:]' '[:lower:]') if echo "$LOWER" | grep -qE 'skip.?ci|ci.?skip'; then echo "skip=true" >> $GITHUB_OUTPUT else echo "skip=false" >> $GITHUB_OUTPUT - fi + fi + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} fetch-translations: strategy: @@ -221,12 +222,13 @@ jobs: - name: Truncate commit message for release name id: release-name run: | - RAW="${{ github.event.head_commit.message }}" - # Take only the first line (subject), then truncate to safe length - SUBJECT=$(echo "$RAW" | head -n 1) - MAX=220 # leave room for tag prefix + ": " + SUBJECT=$(echo "$COMMIT_MSG" | head -n 1) + MAX=220 TRUNCATED="${SUBJECT:0:$MAX}" - echo "name=${{ needs.create-tag.outputs.build-tag }}: $TRUNCATED" >> $GITHUB_OUTPUT + echo "name=${BUILD_TAG}: $TRUNCATED" >> $GITHUB_OUTPUT + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + BUILD_TAG: ${{ needs.create-tag.outputs.build-tag }} - name: Create Release uses: ncipollo/release-action@v1.14.0 diff --git a/.gitignore b/.gitignore index fe783fa4..7d0e131b 100644 --- a/.gitignore +++ b/.gitignore @@ -436,3 +436,8 @@ FodyWeavers.xsd # SpecStory files /.specstory/ /.vscode/settings.json + +# Other +/Sentry/ +/downloads/ +server.pid diff --git a/.skills/csharp-best-practices/SKILL.md b/.skills/csharp-best-practices/SKILL.md index 8eb731ef..27c67ad4 100644 --- a/.skills/csharp-best-practices/SKILL.md +++ b/.skills/csharp-best-practices/SKILL.md @@ -3,7 +3,6 @@ name: csharp-best-practices description: > C# 14 / .NET 10 coding conventions, idiomatic patterns, and performance best practices for the Minecraft Console Client codebase. Use when writing, reviewing, or modifying C# code. -version: 0.4.0 --- # C# 14 / .NET 10 Best Practices diff --git a/.skills/csharp-dotnet-cli-optimization/SKILL.md b/.skills/csharp-dotnet-cli-optimization/SKILL.md index 71acf832..7691da85 100644 --- a/.skills/csharp-dotnet-cli-optimization/SKILL.md +++ b/.skills/csharp-dotnet-cli-optimization/SKILL.md @@ -31,7 +31,6 @@ metadata: - slow - hang - deadlock -version: 0.2.0 --- # C#/.NET CLI Optimization diff --git a/.skills/csharp-optimization/SKILL.md b/.skills/csharp-optimization/SKILL.md index 060b4d6b..9caf92f9 100644 --- a/.skills/csharp-optimization/SKILL.md +++ b/.skills/csharp-optimization/SKILL.md @@ -7,7 +7,6 @@ metadata: category: technique triggers: performance, allocations, GC, hot path, latency, throughput, memory pressure, optimize, slow, freeze, lag spike, packet processing speed -version: 0.2.0 --- # C# Performance Optimization for MCC diff --git a/.skills/humanizer/SKILL.md b/.skills/humanizer/SKILL.md index 45e2cb0c..9609cb69 100644 --- a/.skills/humanizer/SKILL.md +++ b/.skills/humanizer/SKILL.md @@ -1,6 +1,5 @@ --- name: humanizer -version: 2.1.1 description: | Remove signs of AI-generated writing from text. Use when editing or reviewing text to make it sound more natural and human-written. Based on Wikipedia's diff --git a/.skills/mcc-dev-workflow/SKILL.md b/.skills/mcc-dev-workflow/SKILL.md index f1a3c8fe..b1a9ef01 100644 --- a/.skills/mcc-dev-workflow/SKILL.md +++ b/.skills/mcc-dev-workflow/SKILL.md @@ -1,6 +1,6 @@ --- name: mcc-dev-workflow -description: Build, run, and debug Minecraft Console Client (MCC) against a real local Minecraft Java server in WSL. Use this whenever the user wants to compile MCC, start or inspect a local test server, connect MCC to a server, debug protocol or login issues, validate a code change end-to-end, or run MCC commands on a real server instead of guessing from static code. +description: Build, run, and debug Minecraft Console Client (MCC) against a real local Minecraft Java server on Linux, macOS, or WSL. Use this whenever the user wants to compile MCC, start or inspect a local test server, connect MCC to a server, debug protocol or login issues, validate a code change end-to-end, or run MCC commands on a real server instead of guessing from static code. --- # MCC Development Workflow @@ -11,7 +11,7 @@ Use this skill when the task needs a real local server loop, not just code readi - Solution: `MinecraftClient.sln` - Runtime target: `.NET 10` / `net10.0` -- Environment: WSL Ubuntu, Java 21, tmux, python3 +- Environment: Linux, macOS, or WSL with Java, tmux, python3, and dotnet available - Default server root: `${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}` - Default validation target when the user does not specify a version: `1.21.11` @@ -30,10 +30,22 @@ Both modes support the same commands and input/output through `ConsoleIO.Backend - Prefer a real local server over static reasoning for protocol, login, movement, inventory, entity, or command-path work. - Treat tmux `mc-*` sessions as shared state. Do not run multi-version server workflows in parallel unless the harness explicitly isolates them. -- For scripted or repeatable runs, prefer a temporary config copied from `MinecraftClient.ini`. Use the repo-root config only for ad hoc manual work. +- For scripted or repeatable runs, use a generated temporary config. Do not edit the repo-root `MinecraftClient.ini` as part of the test loop. - A server log line containing `Done (` means startup finished. It does not guarantee that RCON is ready on the first attempt. Retry early `mc-rcon` commands. - When instructions, docs, and code disagree, trust current code and current tool behavior first. +## Preflight and reset + +Before scripted runs, especially on macOS or in a reused tmux environment: + +```bash +source tools/mcc-env.sh +mcc-preflight 1.21.11 +mc-reset-test-env 1.21.11 +``` + +`mcc-preflight` checks Java, tmux, dotnet, python3, and server directories. It also resolves common Homebrew Java paths on macOS. `mc-reset-test-env` clears stale tmux sessions and stale `stdin.pipe` files before they turn into misleading startup failures. + ## Build ```bash @@ -92,7 +104,7 @@ mcc-debug -v 1.21.11 --file-input --no-build ### What mcc-debug.sh does 1. Builds MCC (unless `--no-build`) -2. Creates a temp config at `/tmp/mcc-debug/MinecraftClient.debug.ini` with CursorBot account, Terrain/Inventory/Entity enabled +2. Creates a clean temp config at `/tmp/mcc-debug/MinecraftClient.debug.ini` with CursorBot account, Terrain/Inventory/Entity enabled and noisy bots disabled 3. Ensures server is running (starts if not, waits for `Done (`) 4. Launches MCC in the specified mode @@ -210,6 +222,9 @@ After `source tools/mcc-env.sh`: | `mc-rcon "CMD"` | Send RCON command | | `mc-kill VER` | Force-kill server tmux session | | `mc-list` | List running MC server sessions | +| `mc-wait-ready VER [SEC]` | Wait for server `Done (` | +| `mc-wait-stop VER [SEC]` | Wait for server shutdown, with force-kill fallback | +| `mc-reset-test-env [--all|VER...]` | Reset shared tmux server state and stale pipes | | `mcc-build` | Build MCC | | `mcc-run [PORT]` | Run MCC classic+FileInput on port | | `mcc-tui [PORT]` | Run MCC TUI mode in tmux | @@ -218,6 +233,7 @@ After `source tools/mcc-env.sh`: | `mcc-debug [OPTS]` | One-step debug session (see above) | | `mcc-log-mcc` | Tail MCC debug log | | `mcc-state` | Send `debug state` and print last 30 log lines | +| `mcc-preflight [VER...]` | Verify Java, tmux, dotnet, python3, and server dirs | ## Temporary config recipe @@ -226,14 +242,10 @@ source tools/mcc-env.sh TEST_ROOT="${TMPDIR:-/tmp}/mcc-dev" CFG="$TEST_ROOT/MinecraftClient.1.21.11.ini" mkdir -p "$TEST_ROOT" -cp "$MCC_REPO/MinecraftClient.ini" "$CFG" -sed -i \ - -e 's/Account = { Login = "test", Password = "-" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e 's/MinecraftVersion = "auto"/MinecraftVersion = "1.21.11"/' \ - -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ - -e 's/InventoryHandling = false/InventoryHandling = true/' \ - -e 's/EntityHandling = false/EntityHandling = true/' \ - "$CFG" +bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \ + "$CFG" \ + "1.21.11" \ + "CursorBot" ``` For TUI mode, also add: @@ -257,6 +269,8 @@ Basic command check: mcc-cmd "inventory player list" ``` +If a scripted run fails before MCC joins, check for a harness problem before assuming a product regression. Missing `mcc.log`, a pre-join `Connection refused`, or a server that never reached `Done (` usually means shared-state cleanup or startup failed. + ## Typical debug loop 1. `source tools/mcc-env.sh` diff --git a/.skills/mcc-integration-testing/SKILL.md b/.skills/mcc-integration-testing/SKILL.md index 50673abb..168b545f 100644 --- a/.skills/mcc-integration-testing/SKILL.md +++ b/.skills/mcc-integration-testing/SKILL.md @@ -56,7 +56,7 @@ If the environment cannot run a real server, say so and report the result as une - Use a real local server. - Launch MCC against an explicit `localhost:` target for repeatable local tests. - Keep version matrices sequential in shared local environments. The tmux server harness is shared state by default. -- Prefer temporary MCC configs for scripted runs so one test does not contaminate the next. +- Prefer generated temporary MCC configs for scripted runs so one test does not contaminate the next. - Default to offline auth in generated temp configs. Do not trust the repo-root `MinecraftClient.ini` account defaults. - If the user explicitly asks for Microsoft online login, honor that request and generate the temp config for Microsoft auth instead of offline mode. - For Microsoft auth, prefer an interactive TTY launch with `BasicIO-NoColor` so the device code is easy to read and relay to the user. @@ -65,8 +65,10 @@ If the environment cannot run a real server, say so and report the result as une - Legacy and modern command syntax differ. Do not assume one server-command profile fits every version. - Use actual MCC output and actual server logs for assertions. Do not invent success strings. - Treat server `Done` as startup progress, not RCON readiness. Retry the first RCON command before assuming the setup is broken. +- Run preflight before scripted test loops. On macOS, Java may be installed but not exported on PATH in the shell the harness uses. - If a change touches shared routing or a version range, test at least one adjacent version that shares that path, or explicitly mark adjacent versions as unexecuted and inferred. - For palette or version-content changes, probe at least one neighboring or existing item, entity, or block. Do not only check the headline addition. +- Separate product failures from harness failures. Missing logs, stale tmux state, stale `stdin.pipe`, or pre-join `Connection refused` errors are usually environment problems until proven otherwise. ## Choose the test mode @@ -117,11 +119,19 @@ Run them against a real server with a temp config and summarize counts from the Before running any scenario: +0. run preflight and clear stale shared state when the environment is reused 1. configure the target server for offline testing 2. ensure `eula=true` 3. ensure RCON is enabled 4. build MCC unless the task explicitly reuses a fresh build +Preflight and reset helpers: + +```bash +.skills/mcc-integration-testing/scripts/preflight_test_env.sh 1.21.11-Vanilla +.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh 1.21.11-Vanilla +``` + Offline configuration helper: ```bash @@ -141,8 +151,12 @@ Optionally override the login name with the fourth argument to the config helper - `.skills/mcc-integration-testing/scripts/ensure_offline_server.sh` - configures persistent offline mode and RCON +- `.skills/mcc-integration-testing/scripts/preflight_test_env.sh` + - verifies Java, tmux, dotnet, python3, server directories, and resolves common Java PATH issues +- `.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh` + - clears stale tmux sessions and stale `stdin.pipe` files before a rerun - `.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh` - - copies `MinecraftClient.ini`, prepares offline login by default, and can switch to Microsoft auth when explicitly requested + - generates a clean temporary MCC config, prepares offline login by default, disables noisy bots, and can switch to Microsoft auth when explicitly requested - `.skills/mcc-integration-testing/scripts/get_server_port.sh` - resolves the actual local server port from `server.properties` or the latest server log - `.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh` @@ -159,6 +173,7 @@ In every report, separate: - `Executed`: exact scripts, commands, versions, auth mode, and whether the run was sequential or single-version - `Observed`: exact MCC output, exact server-log evidence, and the saved log directory - `Inferred`: conclusions not directly shown by that run's runtime evidence +- `Harness issues`: setup or runner problems such as missing Java on PATH, stale tmux sessions, stale `stdin.pipe`, missing log artifacts, or failed config generation Never upgrade inferred claims to observed facts. Absence of errors is supporting evidence only; pair it with a positive assertion for the feature under test. @@ -196,6 +211,7 @@ Always summarize: ## Troubleshooting - If the first RCON command fails, retry it before assuming the setup is broken. +- If Java is installed but the harness still says it is missing, run `preflight_test_env.sh`. This resolves common Homebrew Java paths on macOS. - If MCC reaches Microsoft device-code login during an offline test, stop and inspect the generated temp config before retrying. - If the user explicitly requests Microsoft online login, set `MCC_TEST_ACCOUNT_TYPE=microsoft` before launching the harness. - If the user explicitly requests Microsoft online login, use `BasicIO-NoColor` in a real TTY, relay the device code from the TUI, and avoid pressing empty Enter at any auth prompt. @@ -203,7 +219,8 @@ Always summarize: - If `dotnet run` cannot see an existing Microsoft session, check whether `SessionCache.db` and `ProfileKeyCache.ini` need to be synced from `MinecraftClient/bin/Release/net10.0/` to the repo root. - If Microsoft auth keeps prompting even with a valid session cache, verify `Account.Login` matches the cached username exactly. - If MCC reports `Connection refused`, verify the launched target matches the server's actual `server-port`. +- If MCC reports `Connection refused` immediately after a server start, also check for stale shared state: old tmux sessions, a stale `stdin.pipe`, or a server that never actually reached `Done (`. - If multiple versions are being tested, do not start them in parallel unless the harness isolates tmux sessions and input files. - If a test assertion fails, inspect the real MCC output before changing the code or weakening the assertion. - If an older server behaves oddly on Linux, check `use-native-transport=false` in `server.properties`. -- If a test should be repeatable, avoid mutating the repo-root `MinecraftClient.ini`. +- If a matrix row fails before producing `mcc.log` or a command transcript, treat it as a harness failure, fix the environment, and rerun that row before drawing product conclusions. diff --git a/.skills/mcc-integration-testing/scripts/common.sh b/.skills/mcc-integration-testing/scripts/common.sh new file mode 100755 index 00000000..973b5da3 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/common.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +sed_in_place() { + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' "$@" + else + sed -i "$@" + fi +} + +ensure_java_in_path() { + if command -v java >/dev/null 2>&1 && java -version >/dev/null 2>&1; then + return 0 + fi + + local candidate + for candidate in \ + "${JAVA_BIN:-}" \ + "/opt/homebrew/opt/openjdk/bin/java" \ + "/usr/local/opt/openjdk/bin/java" \ + "/usr/lib/jvm/default-java/bin/java" + do + [[ -z "$candidate" ]] && continue + if [[ -x "$candidate" ]]; then + export PATH="$(dirname "$candidate"):$PATH" + export JAVA_BIN="$candidate" + if java -version >/dev/null 2>&1; then + return 0 + fi + fi + done + + echo "java was not found on PATH. Install Java or set JAVA_BIN." >&2 + return 1 +} + +server_session_name() { + printf 'mc-%s\n' "${1//./_}" +} + +server_running() { + local version="$1" + mc-list | grep -Fq "$(server_session_name "$version")" +} + +wait_for_server_ready() { + local version="$1" + local timeout="${2:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if mc-log "$version" 250 2>/dev/null | grep -Fq "Done ("; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for $version to become ready" >&2 + return 1 +} + +wait_for_server_stop() { + local version="$1" + local timeout="${2:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if ! server_running "$version"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + mc-kill "$version" >/dev/null 2>&1 || true + + if ! server_running "$version"; then + return 0 + fi + + echo "Timed out waiting for $version to stop" >&2 + return 1 +} + +disable_noisy_bots_in_ini() { + local ini_file="$1" + local section + + for section in \ + ScriptScheduler \ + DiscordRpc \ + AntiAFK \ + AutoDig \ + AutoAttack \ + PlayerListLogger \ + ReplayCapture + do + sed_in_place "/^\\[ChatBot\\.${section}\\]/,/^\\[/ { s/^Enabled = true/Enabled = false/; }" "$ini_file" + done +} + +remove_stale_stdin_pipe() { + local version="$1" + local pipe_path="$MCC_SERVERS/$version/stdin.pipe" + + if [[ -e "$pipe_path" && ! -p "$pipe_path" ]]; then + rm -f "$pipe_path" + fi +} diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh index 5e67687d..38e73978 100755 --- a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" VERSION="${1:-1.21.11-Vanilla}" SERVER_DIR="${MCC_SERVERS:?}/$VERSION" @@ -25,40 +27,12 @@ server_running() { mc-list | grep -Fq "$SESSION_NAME" } -wait_for_server_ready() { - local timeout="${1:-60}" - local elapsed=0 - while (( elapsed < timeout )); do - if mc-log "$VERSION" 200 2>/dev/null | grep -Fq "Done ("; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - echo "Timed out waiting for $VERSION to become ready" >&2 - return 1 -} - -wait_for_server_stop() { - local timeout="${1:-60}" - local elapsed=0 - while (( elapsed < timeout )); do - if ! server_running; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - echo "Timed out waiting for $VERSION to stop" >&2 - return 1 -} - upsert_property() { local key="$1" local value="$2" if grep -Eq "^${key}=" "$PROPS_FILE"; then - sed -i "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE" + sed_in_place "s#^${key}=.*#${key}=${value}#" "$PROPS_FILE" else printf '%s=%s\n' "$key" "$value" >> "$PROPS_FILE" fi @@ -66,14 +40,14 @@ upsert_property() { if [[ ! -f "$PROPS_FILE" ]]; then mc-start "$VERSION" - wait_for_server_ready + wait_for_server_ready "$VERSION" mc-stop "$VERSION" - wait_for_server_stop + wait_for_server_stop "$VERSION" fi if server_running; then mc-stop "$VERSION" - wait_for_server_stop + wait_for_server_stop "$VERSION" fi upsert_property "online-mode" "false" diff --git a/.skills/mcc-integration-testing/scripts/preflight_test_env.sh b/.skills/mcc-integration-testing/scripts/preflight_test_env.sh new file mode 100755 index 00000000..22376026 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/preflight_test_env.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' +Usage: preflight_test_env.sh [server-dir...] + +Checks the local MCC test environment and resolves common Java path issues. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +ensure_java_in_path +command -v tmux >/dev/null 2>&1 || { echo "tmux was not found on PATH." >&2; exit 1; } +command -v dotnet >/dev/null 2>&1 || { echo "dotnet was not found on PATH." >&2; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "python3 was not found on PATH." >&2; exit 1; } + +if [[ ! -d "$MCC_SERVERS" ]]; then + echo "Server root not found: $MCC_SERVERS" >&2 + exit 1 +fi + +for server_dir in "$@"; do + [[ -z "$server_dir" ]] && continue + if [[ ! -d "$MCC_SERVERS/$server_dir" ]]; then + echo "Server directory not found: $MCC_SERVERS/$server_dir" >&2 + exit 1 + fi + + remove_stale_stdin_pipe "$server_dir" +done + +printf 'MCC_REPO=%s\n' "$MCC_REPO" +printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" +printf 'JAVA=%s\n' "$(command -v java)" +printf 'TMUX=%s\n' "$(command -v tmux)" +printf 'DOTNET=%s\n' "$(command -v dotnet)" diff --git a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh index f36129fa..64727a58 100644 --- a/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh +++ b/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh @@ -1,15 +1,40 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -lt 3 || $# -gt 4 ]]; then - echo "Usage: $0 [login]" >&2 +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' >&2 +Usage: + prepare_offline_mcc_config.sh [login] + prepare_offline_mcc_config.sh [login] +EOF +} + +if [[ $# -lt 2 || $# -gt 4 ]]; then + usage exit 1 fi -TEMPLATE_INI="$1" -OUTPUT_INI="$2" -MC_VERSION="$3" -LOGIN_NAME="${4:-CursorBot}" +TEMPLATE_INI="" +OUTPUT_INI="" +MC_VERSION="" +LOGIN_NAME="" + +if [[ $# -ge 3 && -f "$1" ]]; then + TEMPLATE_INI="$1" + OUTPUT_INI="$2" + MC_VERSION="$3" + LOGIN_NAME="${4:-CursorBot}" +else + OUTPUT_INI="$1" + MC_VERSION="$2" + LOGIN_NAME="${3:-CursorBot}" +fi + ACCOUNT_TYPE="${MCC_TEST_ACCOUNT_TYPE:-mojang}" PASSWORD_VALUE="${MCC_TEST_PASSWORD-}" @@ -26,9 +51,35 @@ if [[ -z "${MCC_TEST_PASSWORD+x}" ]]; then fi fi +generate_template_ini() { + local template_root + template_root="$(mktemp -d "${TMPDIR:-/tmp}/mcc-config-template.XXXXXX")" + + if [[ ! -f "$REPO_ROOT/MinecraftClient/bin/Release/net10.0/MinecraftClient.dll" ]]; then + dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo >/dev/null + fi + + ( + cd "$template_root" + dotnet run --project "$REPO_ROOT/MinecraftClient" -c Release --no-build -- --help >/dev/null 2>&1 + ) + + if [[ ! -f "$template_root/MinecraftClient.ini" ]]; then + echo "Failed to generate a temporary MCC config template." >&2 + exit 1 + fi + + TEMPLATE_INI="$template_root/MinecraftClient.ini" +} + +if [[ -z "$TEMPLATE_INI" ]]; then + generate_template_ini +fi + +mkdir -p "$(dirname "$OUTPUT_INI")" cp "$TEMPLATE_INI" "$OUTPUT_INI" -sed -i \ +sed_in_place \ -e "s#^Account = .*#Account = { Login = \"$LOGIN_NAME\", Password = \"$PASSWORD_VALUE\" }#" \ -e "s#^AccountType = .*#AccountType = \"$ACCOUNT_TYPE\"#" \ -e "s#^MinecraftVersion = \"[^\"]*\"\\(.*\\)\$#MinecraftVersion = \"$MC_VERSION\"\\1#" \ @@ -38,6 +89,8 @@ sed -i \ -e 's#^AutoRespawn = false#AutoRespawn = true#' \ "$OUTPUT_INI" +disable_noisy_bots_in_ini "$OUTPUT_INI" + grep -Fq "AccountType = \"$ACCOUNT_TYPE\"" "$OUTPUT_INI" || { echo "Failed to enforce account type $ACCOUNT_TYPE in $OUTPUT_INI" >&2 exit 1 diff --git a/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh new file mode 100755 index 00000000..2d84ac1b --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' +Usage: reset_shared_test_state.sh [--all | ...] + +Kills shared tmux test sessions and removes stale stdin pipes. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +kill_named_session() { + local session_name="$1" + tmux kill-session -t "$session_name" 2>/dev/null || true +} + +kill_named_session "mcc-debug" + +if [[ $# -eq 0 || "${1:-}" == "--all" ]]; then + while IFS= read -r session_name; do + [[ -z "$session_name" ]] && continue + kill_named_session "$session_name" + done < <(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true) + + while IFS= read -r pipe_path; do + [[ -z "$pipe_path" ]] && continue + if [[ ! -p "$pipe_path" ]]; then + rm -f "$pipe_path" + fi + done < <(find "$MCC_SERVERS" -maxdepth 2 -name 'stdin.pipe' 2>/dev/null || true) +else + for version in "$@"; do + kill_named_session "$(server_session_name "$version")" + remove_stale_stdin_pipe "$version" + done +fi + +rm -f "$MCC_REPO/mcc_input.txt" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh new file mode 100755 index 00000000..65ff7d3f --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" + +RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements/matrix" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +MATRIX_DIR="$RUN_ROOT/$RUN_ID" +RESULTS_TSV="$MATRIX_DIR/results.tsv" +BUILD_LOG="$MATRIX_DIR/build.log" +REPORT_MD="$MATRIX_DIR/report.md" +PRECHECK_TXT="$MATRIX_DIR/preflight.txt" + +mkdir -p "$MATRIX_DIR" + +write_row() { + local fields=("$@") + + while (( ${#fields[@]} < 14 )); do + fields+=("") + done + + printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \ + "${fields[0]}" "${fields[1]}" "${fields[2]}" "${fields[3]}" "${fields[4]}" "${fields[5]}" "${fields[6]}" \ + "${fields[7]}" "${fields[8]}" "${fields[9]}" "${fields[10]}" "${fields[11]}" "${fields[12]}" \ + "${fields[13]}" >> "$RESULTS_TSV" +} + +resolve_server_dir() { + local version="$1" + local candidate + + for candidate in "$version" "$version-Vanilla"; do + if [[ -d "$MCC_SERVERS/$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +run_version() { + local version="$1" + local profile="$2" + local family="$3" + local server_dir="$4" + local summary_env + + if bash "$SCRIPT_DIR/run_achievements_test.sh" --no-build "$server_dir" "$version" "$profile"; then + : + fi + + summary_env="${TMPDIR:-/tmp}/mcc-achievements/$server_dir/latest/summary.env" + if [[ ! -f "$summary_env" ]]; then + write_row "$version" "$server_dir" "unknown" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "Summary file was not produced." "" "" "" + return + fi + + # shellcheck disable=SC1090 + source "$summary_env" + + if [[ -n "${MCC_LOG:-}" && ! -f "$MCC_LOG" ]]; then + NOTE="Harness failure: MCC log was not produced." + VERDICT="❌ Fail" + fi + + if [[ -n "${COMMAND_LOG:-}" && ! -f "$COMMAND_LOG" ]]; then + NOTE="Harness failure: command transcript was not produced." + VERDICT="❌ Fail" + fi + + write_row "$VERSION" "$SERVER_DIR" "$PORT" "$FAMILY" "$INITIAL_STATUS" "$GRANT_STATUS" "$REVOKE_STATUS" \ + "$API_STATUS" "$VERDICT" "$NOTE" "$RUN_DIR" "$MCC_LOG" "$COPIED_SERVER_LOG" "$COMMAND_LOG" +} + +{ + printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" + printf 'RUN_DIR=%s\n' "$MATRIX_DIR" + printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" +} > "$PRECHECK_TXT" + +printf 'Version\tServerDir\tPort\tFamily\tInitial\tGrant\tRevoke\tAPI\tVerdict\tNote\tRunDir\tMccLog\tServerLog\tCommandLog\n' > "$RESULTS_TSV" + +JAVA_OK="yes" +TMUX_OK="yes" +DOTNET_OK="yes" +BUILD_OK="yes" + +if ! command -v dotnet >/dev/null 2>&1; then + DOTNET_OK="no" +fi + +if ! command -v java >/dev/null 2>&1 || ! java -version >/dev/null 2>&1; then + JAVA_OK="no" +fi + +if ! command -v tmux >/dev/null 2>&1; then + TMUX_OK="no" +fi + +if [[ "$DOTNET_OK" == "yes" ]]; then + bash "$SCRIPT_DIR/preflight_test_env.sh" >/dev/null 2>&1 || true + if ! dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release > "$BUILD_LOG" 2>&1; then + BUILD_OK="no" + fi +else + : > "$BUILD_LOG" +fi + +{ + printf 'MCC_SERVERS=%s\n' "$MCC_SERVERS" + printf 'RUN_DIR=%s\n' "$MATRIX_DIR" + printf 'DATE=%s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" + printf 'dotnet=%s\n' "$DOTNET_OK" + printf 'java=%s\n' "$JAVA_OK" + printf 'tmux=%s\n' "$TMUX_OK" + printf 'build=%s\n' "$BUILD_OK" +} > "$PRECHECK_TXT" + +while IFS='|' read -r version profile family; do + [[ -z "$version" ]] && continue + + if [[ "$DOTNET_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "dotnet is not available on PATH." + continue + fi + + if [[ "$BUILD_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "dotnet build failed. See $BUILD_LOG." + continue + fi + + if [[ "$JAVA_OK" != "yes" || "$TMUX_OK" != "yes" ]]; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "❌ Fail" \ + "java or tmux is not available, so live server execution was blocked." + continue + fi + + if ! server_dir="$(resolve_server_dir "$version")"; then + write_row "$version" "" "" "$family" "❌" "❌" "❌" "❌" "⚠️ Partial" \ + "Server directory for $version was not found under $MCC_SERVERS." + continue + fi + + run_version "$version" "$profile" "$family" "$server_dir" +done <<'EOF' +1.8|legacy|Legacy 🧱 +1.11.2|legacy|Legacy 🧱 +1.12.2|modern|First advancements 🌱 +1.19.4|modern|Stable modern ✅ +1.20|modern|Telemetry edge 1 ⚠️ +1.20.2|modern|Telemetry edge 2 ⚠️ +1.20.4|modern|End of 1.20.x ⚠️ +1.20.6|modern|Post-1.20.6 🔧 +1.21.2|modern|1.21.2 family 🔧 +1.21.11|modern|showAdvancements 🆕 +26.1|modern|Latest supported 🚀 +EOF + +bash "$SCRIPT_DIR/summarize_achievements_matrix.sh" "$MATRIX_DIR" > "$REPORT_MD" +printf '%s\n' "$MATRIX_DIR" diff --git a/.skills/mcc-integration-testing/scripts/run_achievements_test.sh b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh new file mode 100755 index 00000000..df0236e0 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_achievements_test.sh @@ -0,0 +1,396 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +# shellcheck source=tools/mcc-env.sh +source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" + +usage() { + cat <<'EOF' +Usage: run_achievements_test.sh [--no-build] + +Examples: + .skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.8 1.8 legacy + .skills/mcc-integration-testing/scripts/run_achievements_test.sh --no-build 1.21.11-Vanilla 1.21.11 modern +EOF +} + +DO_BUILD=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --no-build) DO_BUILD=false; shift ;; + --build) DO_BUILD=true; shift ;; + -h|--help) usage; exit 0 ;; + *) break ;; + esac +done + +if [[ $# -ne 3 ]]; then + usage >&2 + exit 1 +fi + +SERVER_DIR="$1" +MC_VERSION="$2" +PROFILE="$3" + +if [[ "$PROFILE" != "legacy" && "$PROFILE" != "modern" ]]; then + echo "Unsupported profile: $PROFILE" >&2 + exit 1 +fi + +RUN_ROOT="${TMPDIR:-/tmp}/mcc-achievements" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +RUN_DIR="$RUN_ROOT/$SERVER_DIR/$RUN_ID" +LATEST_LINK="$RUN_ROOT/$SERVER_DIR/latest" +MCC_LOG="$RUN_DIR/mcc.log" +BUILD_LOG="$RUN_DIR/build.log" +SERVER_TMUX_LOG="$RUN_DIR/server-tmux.log" +SERVER_FILE_LOG="$RUN_DIR/server-latest.log" +COMMAND_LOG="$RUN_DIR/commands.log" +SUMMARY_ENV="$RUN_DIR/summary.env" +PROBE_SCRIPT="$RUN_DIR/achievement_probe.cs" +CFG="$RUN_DIR/MinecraftClient.$MC_VERSION.ini" +INPUT_FILE="$REPO_ROOT/mcc_input.txt" +SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log" +TARGET_ID="minecraft:story/root" +TARGET_COMMAND_GRANT="advancement grant CursorBot only minecraft:story/root" +TARGET_COMMAND_REVOKE="advancement revoke CursorBot only minecraft:story/root" +TARGET_TYPE="Modern 🌱" +PORT="unknown" +MCC_PID="" + +INITIAL_STATUS="❌" +GRANT_STATUS="❌" +REVOKE_STATUS="❌" +API_STATUS="❌" +VERDICT="❌ Fail" +NOTE="Run did not complete." +EXECUTED="yes" + +if [[ "$PROFILE" == "legacy" ]]; then + TARGET_ID="achievement.openInventory" + TARGET_COMMAND_GRANT="achievement give achievement.openInventory CursorBot" + TARGET_COMMAND_REVOKE="achievement take achievement.openInventory CursorBot" + TARGET_TYPE="Legacy 🧱" +fi + +mkdir -p "$RUN_DIR" + +write_summary() { + { + printf 'VERSION=%q\n' "$MC_VERSION" + printf 'SERVER_DIR=%q\n' "$SERVER_DIR" + printf 'PROFILE=%q\n' "$PROFILE" + printf 'FAMILY=%q\n' "$TARGET_TYPE" + printf 'PORT=%q\n' "$PORT" + printf 'RUN_DIR=%q\n' "$RUN_DIR" + printf 'MCC_LOG=%q\n' "$MCC_LOG" + printf 'SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log" + printf 'SERVER_FILE_LOG=%q\n' "$SERVER_LOG_FILE" + printf 'SERVER_TMUX_LOG=%q\n' "$SERVER_TMUX_LOG" + printf 'COPIED_SERVER_LOG=%q\n' "$RUN_DIR/server-latest.log" + printf 'COMMAND_LOG=%q\n' "$COMMAND_LOG" + printf 'SUMMARY_ENV=%q\n' "$SUMMARY_ENV" + printf 'TARGET_ID=%q\n' "$TARGET_ID" + printf 'INITIAL_STATUS=%q\n' "$INITIAL_STATUS" + printf 'GRANT_STATUS=%q\n' "$GRANT_STATUS" + printf 'REVOKE_STATUS=%q\n' "$REVOKE_STATUS" + printf 'API_STATUS=%q\n' "$API_STATUS" + printf 'VERDICT=%q\n' "$VERDICT" + printf 'NOTE=%q\n' "$NOTE" + printf 'EXECUTED=%q\n' "$EXECUTED" + } > "$SUMMARY_ENV" +} + +capture_server_logs() { + mc-log "$SERVER_DIR" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true + if [[ -f "$SERVER_LOG_FILE" ]]; then + cp "$SERVER_LOG_FILE" "$RUN_DIR/server-latest.log" 2>/dev/null || true + fi +} + +cleanup() { + capture_server_logs + + if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then + echo "quit" >> "$INPUT_FILE" 2>/dev/null || true + sleep 2 + kill "$MCC_PID" 2>/dev/null || true + wait "$MCC_PID" 2>/dev/null || true + fi + + mc-stop "$SERVER_DIR" >/dev/null 2>&1 || true + wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true + ln -sfn "$RUN_DIR" "$LATEST_LINK" + write_summary +} +trap cleanup EXIT + +log_step() { + printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$1" | tee -a "$COMMAND_LOG" +} + +fail() { + NOTE="$1" + VERDICT="❌ Fail" + exit 1 +} + +wait_for_file_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + local timeout="${4:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for: $description" >&2 + return 1 +} + +write_probe_script() { + cat > "$PROBE_SCRIPT" < updated, IReadOnlyList removedIds, bool reset) + { + LogToConsole($"[ACH_TEST] event reset={reset} updated={updated.Count} removed={removedIds.Count}"); + DumpState("event"); + } + + private void DumpState(string origin) + { + Achievement[] all = GetAchievements(); + Achievement[] unlocked = GetUnlockedAchievements(); + Achievement[] locked = GetLockedAchievements(); + Achievement? target = null; + + foreach (Achievement entry in all) + { + if (entry.Id == TargetId) + { + target = entry; + break; + } + } + + string titleState = "missing"; + string completionState = "missing"; + + if (target is not null) + { + titleState = target.Title is null ? "null" : "present"; + completionState = target.IsCompleted ? "done" : "todo"; + } + + LogToConsole($"[ACH_TEST] snapshot origin={origin} all={all.Length} unlocked={unlocked.Length} locked={locked.Length}"); + LogToConsole($"[ACH_TEST] target_state origin={origin} id={TargetId} title={titleState} completed={completionState}"); + } +} +EOF +} + +run_server_command() { + local cmd="$1" + local attempt + + log_step "SERVER> $cmd" + for attempt in 1 2 3 4 5; do + if mc-rcon "$cmd" >/dev/null 2>&1; then + sleep 1 + return 0 + fi + sleep 1 + done + + fail "Server command failed: $cmd" +} + +run_mcc_command() { + local name="$1" + local cmd="$2" + local delay="${3:-2}" + local start_line=0 + local end_line=0 + + if [[ -f "$MCC_LOG" ]]; then + start_line="$(wc -l < "$MCC_LOG")" + fi + + log_step "MCC> $cmd" + echo "$cmd" >> "$INPUT_FILE" + sleep "$delay" + + if [[ -f "$MCC_LOG" ]]; then + end_line="$(wc -l < "$MCC_LOG")" + fi + + if (( end_line > start_line )); then + sed -n "$((start_line + 1)),$((end_line))p" "$MCC_LOG" > "$RUN_DIR/$name.mcc.log" + else + : > "$RUN_DIR/$name.mcc.log" + fi +} + +assert_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + + grep -Fq "$pattern" "$file" || fail "$description" +} + +if $DO_BUILD; then + log_step "BUILD> dotnet build MinecraftClient.sln -c Release" + mcc-build > "$BUILD_LOG" 2>&1 || fail "dotnet build failed." +else + : > "$BUILD_LOG" +fi + +bash "$SCRIPT_DIR/preflight_test_env.sh" "$SERVER_DIR" >/dev/null || fail "Test environment preflight failed." +bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$SERVER_DIR" >/dev/null || fail "Failed to reset shared test state." + +if [[ ! -d "$MCC_SERVERS/$SERVER_DIR" ]]; then + fail "Server directory not found: $MCC_SERVERS/$SERVER_DIR" +fi + +bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null || fail "Failed to prepare temporary MCC config." +PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$SERVER_DIR")" + +"$SCRIPT_DIR/ensure_offline_server.sh" "$SERVER_DIR" +write_probe_script + +if [[ "$PROFILE" == "legacy" && -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then + sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" +fi + +: > "$INPUT_FILE" +rm -f "$MCC_LOG" + +log_step "Starting server $SERVER_DIR on port $PORT" +mc-start "$SERVER_DIR" >/dev/null +wait_for_server_ready "$SERVER_DIR" || fail "Server did not become ready." + +log_step "Starting MCC for $MC_VERSION" +( + cd "$REPO_ROOT" + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + "$CFG" \ + CursorBot \ + - \ + "localhost:$PORT" \ + "--accounttype=mojang" \ + "--minecraftversion=$MC_VERSION" \ + "--terrainandmovements=true" \ + "--inventoryhandling=true" \ + "--entityhandling=true" \ + "--autorespawn=true" \ + "--debugmessages=true" \ + > "$MCC_LOG" 2>&1 +) & +MCC_PID=$! + +wait_for_file_pattern "$MCC_LOG" "Server was successfully joined." "MCC join success" 90 || fail "MCC failed to join." +wait_for_file_pattern "$SERVER_LOG_FILE" "CursorBot joined the game" "server join entry" 30 || fail "Server never logged the join." + +run_server_command "op CursorBot" +run_server_command "gamerule sendCommandFeedback true" +if [[ "$PROFILE" == "modern" ]]; then + run_server_command "gamerule logAdminCommands true" +fi +run_server_command "time set day" +run_server_command "weather clear" + +run_mcc_command "load_probe" "script $PROBE_SCRIPT" 3 +wait_for_file_pattern "$MCC_LOG" "[ACH_TEST] probe initialized" "probe startup" 30 || fail "Probe script did not initialize." + +run_mcc_command "baseline_debug" "debug state" 2 +run_mcc_command "baseline_all" "achievement" 2 +run_mcc_command "baseline_locked" "achievement locked" 2 +run_mcc_command "baseline_unlocked" "achievement unlocked" 2 + +run_server_command "$TARGET_COMMAND_GRANT" +sleep 3 +run_mcc_command "after_grant_all" "achievement" 2 +run_mcc_command "after_grant_unlocked" "achievement unlocked" 2 + +run_server_command "$TARGET_COMMAND_REVOKE" +sleep 3 +run_mcc_command "after_revoke_all" "achievement" 2 +run_mcc_command "after_revoke_locked" "achievement locked" 2 + +assert_pattern "$MCC_LOG" "Achievements/Advancements:" "Achievement command header never appeared." + +if ! grep -Fq "No achievements/advancements received yet." "$RUN_DIR/baseline_all.mcc.log"; then + INITIAL_STATUS="✅" +fi + +if grep -Fq "$TARGET_ID" "$RUN_DIR/after_grant_unlocked.mcc.log" && grep -Fq "[DONE]" "$RUN_DIR/after_grant_unlocked.mcc.log"; then + GRANT_STATUS="✅" +fi + +if [[ "$PROFILE" == "legacy" ]]; then + if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then + REVOKE_STATUS="✅" + fi +else + if grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_locked.mcc.log" && grep -Fq "[TODO]" "$RUN_DIR/after_revoke_locked.mcc.log"; then + REVOKE_STATUS="✅" + elif [[ "$GRANT_STATUS" == "✅" ]] && ! grep -Fq "$TARGET_ID" "$RUN_DIR/after_revoke_all.mcc.log"; then + REVOKE_STATUS="✅" + fi +fi + +if grep -Fq "[ACH_TEST] event" "$MCC_LOG" && grep -Fq "target_state origin=event id=$TARGET_ID title=" "$MCC_LOG"; then + API_STATUS="✅" +fi + +case "$INITIAL_STATUS|$GRANT_STATUS|$REVOKE_STATUS|$API_STATUS" in + "✅|✅|✅|✅") + VERDICT="✅ Pass" + NOTE="All planned achievement checks passed." + ;; + *"✅"*) + VERDICT="⚠️ Partial" + NOTE="At least one achievement phase passed, but the matrix did not fully clear." + ;; + *) + VERDICT="❌ Fail" + NOTE="Achievement checks did not produce the expected evidence." + ;; +esac + +run_mcc_command "quit" "quit" 2 +NOTE="$NOTE Artifacts saved in $RUN_DIR." diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh index 2db7d2a8..51021974 100755 --- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$SCRIPT_DIR/common.sh" VERSION="${1:-1.21.11-Vanilla}" MC_VERSION="${VERSION%-Vanilla}" @@ -34,46 +36,12 @@ cleanup() { fi mc-stop "$VERSION" >/dev/null 2>&1 || true + wait_for_server_stop "$VERSION" 20 >/dev/null 2>&1 || true } trap cleanup EXIT prepare_config() { - bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" >/dev/null -} - -wait_for_file_pattern() { - local file="$1" - local pattern="$2" - local description="$3" - local timeout="${4:-60}" - local elapsed=0 - - while (( elapsed < timeout )); do - if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - echo "Timed out waiting for: $description" >&2 - return 1 -} - -wait_for_server_ready() { - local timeout="${1:-60}" - local elapsed=0 - - while (( elapsed < timeout )); do - if mc-log "$VERSION" 250 2>/dev/null | grep -Fq "Done ("; then - return 0 - fi - sleep 1 - ((elapsed += 1)) - done - - echo "Timed out waiting for server readiness" >&2 - return 1 + bash "$SCRIPT_DIR/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null } wait_for_server_log_pattern() { @@ -101,6 +69,25 @@ capture_server_logs() { fi } +wait_for_file_pattern() { + local file="$1" + local pattern="$2" + local description="$3" + local timeout="${4:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$file" ]] && grep -Fq "$pattern" "$file"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for: $description" >&2 + return 1 +} + fail() { capture_server_logs echo "FAIL: $1" >&2 @@ -146,18 +133,19 @@ run_mcc_command() { sleep 2 } +bash "$SCRIPT_DIR/preflight_test_env.sh" "$VERSION" >/dev/null +bash "$SCRIPT_DIR/reset_shared_test_state.sh" "$VERSION" >/dev/null "$SCRIPT_DIR/ensure_offline_server.sh" "$VERSION" +echo "Building MCC..." +mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" prepare_config SERVER_PORT="$(bash "$SCRIPT_DIR/get_server_port.sh" "$VERSION")" : > "$INPUT_FILE" -echo "Building MCC..." -mcc-build > "$BUILD_LOG" 2>&1 || fail "mcc-build failed" - echo "Starting server..." mc-start "$VERSION" >/dev/null -wait_for_server_ready || fail "Server did not become ready" +wait_for_server_ready "$VERSION" || fail "Server did not become ready" echo "Starting MCC..." ( diff --git a/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh new file mode 100755 index 00000000..6ef72397 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: summarize_achievements_matrix.sh " >&2 + exit 1 +fi + +MATRIX_DIR="$1" +RESULTS_TSV="$MATRIX_DIR/results.tsv" +PRECHECK_TXT="$MATRIX_DIR/preflight.txt" +BUILD_LOG="$MATRIX_DIR/build.log" + +if [[ ! -f "$RESULTS_TSV" ]]; then + echo "Missing results file: $RESULTS_TSV" >&2 + exit 1 +fi + +echo "# Achievements Matrix Report" +echo +echo "## Executed" +echo +if [[ -f "$PRECHECK_TXT" ]]; then + echo '```text' + cat "$PRECHECK_TXT" + echo '```' +fi +echo +echo "- Matrix artifacts: \`$MATRIX_DIR\`" +echo "- Results TSV: \`$RESULTS_TSV\`" +echo "- Build log: \`$BUILD_LOG\`" +echo "- Execution mode: sequential" +echo "- Auth mode: offline" +echo +echo "## Observed" +echo +echo "| Version | Port | Family | Initial snapshot | Grant | Revoke | API callback | Verdict |" +echo "|---|---:|---|---|---|---|---|---|" +awk -F '\t' 'NR > 1 { + printf("| `%s` | `%s` | %s | %s | %s | %s | %s | %s |\n", + $1, $3, $4, $5, $6, $7, $8, $9); +}' "$RESULTS_TSV" + +echo +echo "## Artifact Links" +echo +awk -F '\t' 'NR > 1 { + printf("- `%s`: run=`%s`, mcc=`%s`, server=`%s`, commands=`%s`\n", $1, $11, $12, $13, $14); + printf(" note: %s\n", $10); +}' "$RESULTS_TSV" + +echo +echo "## Inferred" +echo +echo "- Only rows with real MCC and server-log artifacts count as executed proof." +echo "- Rows blocked by missing Java, tmux, or server directories are environment-limited, not product pass results." +echo "- Rows with missing MCC or command-log artifacts should be treated as harness failures until rerun confirms a product issue." diff --git a/.skills/mcc-version-adaptation/SKILL.md b/.skills/mcc-version-adaptation/SKILL.md index ce21cf04..706399cb 100644 --- a/.skills/mcc-version-adaptation/SKILL.md +++ b/.skills/mcc-version-adaptation/SKILL.md @@ -15,6 +15,7 @@ Systematic workflow for updating Minecraft Console Client to support a new Minec $MCC_REPO/tools/decompile.sh --version ``` This auto-downloads `MinecraftDecompiler.jar` if needed, produces the decompiled source, and downloads `server.jar` into `$MCC_SERVERS//`. +- `tools/decompile.sh` depends on official mappings. For older versions where it refuses to decompile, fall back to a raw Java decompiler such as `cfr-decompiler` against `$MCC_SERVERS//server.jar`. That fallback is good enough for packet inspection and registration order checks even when the output is obfuscated. - A test server of the target version in `$MCC_SERVERS//` (see `mcc-dev-workflow` skill) ## Step 0: Generate Server Reports (CRITICAL since 1.21.9) @@ -215,7 +216,42 @@ The JSON maps block names (snake_case) → collision shape IDs → AABB coordina **Data source**: PrismarineJS `minecraft-data` repo, path: `data/pc//blockCollisionShapes.json`. Version availability can be checked via `data/dataPaths.json`. -## Step 9: Compile and Verify +## Step 9: Update Minimap Block Color Map + +Regenerate the block-to-MapColor mapping used by the TUI minimap. This maps each block's `Material` enum to the RGB color from Minecraft's official `MapColor` table. + +```bash +python3 $MCC_REPO/tools/gen_block_color_map.py $MCC_REPO/MinecraftOfficial/-decompiled +# e.g. python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled +``` + +Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`). + +The script parses `MapColor.java`, `DyeColor.java`, and `Blocks.java` from the decompiled source to extract each block's assigned map color. Blocks not matched to a known `Material` enum value are skipped. + +**When to update**: Whenever new blocks are added or existing blocks change their `mapColor()` assignment. If only items or entities changed, this step can be skipped. + +## Step 10: Update Minimap Entity Categories + +Regenerate the entity-to-MobCategory mapping used by the TUI minimap for classifying entities as hostile, passive, neutral, or non-living. + +```bash +python3 $MCC_REPO/tools/gen_entity_category_map.py $MCC_REPO/MinecraftOfficial/-decompiled +# e.g. python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled +``` + +Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`). + +The script parses `EntityType.java` to extract each entity's `MobCategory` assignment, then maps Minecraft's categories to MCC minimap categories: +- `MONSTER` -> hostile (with neutral overrides for conditionally hostile mobs like Enderman, Spider, Wolf) +- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive +- `MISC` -> non_living (with passive overrides for Villager, WanderingTrader, ZombieHorse) + +The script maintains manual override lists for "neutral" mobs (attack only when provoked) since Minecraft has no machine-readable flag for this behavior. Review and update the `NEUTRAL_OVERRIDES` and `PASSIVE_OVERRIDES` sets in the script when new conditionally-hostile or misclassified mobs are added. + +**When to update**: Whenever new entity types are added. If only blocks or items changed, this step can be skipped. + +## Step 11: Compile and Verify ```bash dotnet build $MCC_REPO/MinecraftClient.sln -c Release @@ -274,3 +310,5 @@ All scripts are in `$MCC_REPO/tools/`. See `tools/README.md` for detailed usage. | `gen_entity_palette.py` | Generate EntityPalette C# | registries.json | | `gen_entity_metadata_palette.py` | Generate EntityMetadataPalette C# | Decompiled source | | `gen_block_shapes.py` | Download & compact block collision shapes | PrismarineJS minecraft-data | +| `gen_block_color_map.py` | Generate minimap block color JSON | Decompiled source (MapColor/DyeColor/Blocks) | +| `gen_entity_category_map.py` | Generate minimap entity category JSON | Decompiled source (EntityType.java) | diff --git a/.skills/writing-skills/SKILL.md b/.skills/writing-skills/SKILL.md index c00da178..514e2c4f 100644 --- a/.skills/writing-skills/SKILL.md +++ b/.skills/writing-skills/SKILL.md @@ -1,10 +1,6 @@ --- name: writing-skills description: "Use when creating, updating, or improving agent skills." -category: meta -risk: unknown -source: community -date_added: "2026-02-27" --- # Writing Skills (Excellence) diff --git a/AGENTS.md b/AGENTS.md index 40f216f7..fec9d8f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ - Minecraft Console Client (MCC) is a cross-platform text/TUI client for Minecraft Java Edition. - Primary scope: connect to servers, send chat and commands, receive text, automate gameplay/admin tasks, and extend behavior through built-in bots or runtime C# scripts. - Secondary scope: protocol/version adaptation tooling, docs site, legacy GUI wrapper, and debug tooling. +- Decompiled server source for both the old and new MC versions in `$MCC_REPO/MinecraftOfficial/-decompiled/` ## Build / Run - Init submodules first: `git submodule update --init --recursive` diff --git a/MinecraftClient/Achievement.cs b/MinecraftClient/Achievement.cs new file mode 100644 index 00000000..760e4054 --- /dev/null +++ b/MinecraftClient/Achievement.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace MinecraftClient +{ + /// + /// The type of an achievement or advancement. + /// + public enum AchievementType + { + Task, + Challenge, + Goal, + Legacy + } + + /// + /// Represents a Minecraft achievement (pre-1.12) or advancement (1.12+). + /// + /// Resource identifier, e.g. "minecraft:story/root" or "achievement.openInventory" + /// Display title (null for legacy achievements without display info) + /// Display description (null for legacy achievements without display info) + /// The frame type / achievement category + /// Whether this advancement is hidden in the UI + /// Whether all requirements have been met + /// OR-groups of criterion names; all groups must be satisfied + /// Per-criterion completion status + public record Achievement( + string Id, + string? Title, + string? Description, + AchievementType Type, + bool IsHidden, + bool IsCompleted, + IReadOnlyList> Requirements, + IReadOnlyDictionary CriteriaProgress); +} diff --git a/MinecraftClient/ChatBots/AutoDig.cs b/MinecraftClient/ChatBots/AutoDig.cs index 67d0b199..148335d5 100644 --- a/MinecraftClient/ChatBots/AutoDig.cs +++ b/MinecraftClient/ChatBots/AutoDig.cs @@ -5,7 +5,9 @@ using System.Threading; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; using MinecraftClient.CommandHandler.Patch; +using MinecraftClient.Inventory; using MinecraftClient.Mapping; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; using MinecraftClient.Scripting; using Tomlet.Attributes; @@ -25,15 +27,12 @@ namespace MinecraftClient.ChatBots public bool Enabled = false; - [NonSerialized] [TomlInlineComment("$ChatBot.AutoDig.Auto_Tool_Switch$")] public bool Auto_Tool_Switch = false; - [NonSerialized] [TomlInlineComment("$ChatBot.AutoDig.Durability_Limit$")] public int Durability_Limit = 2; - [NonSerialized] [TomlInlineComment("$ChatBot.AutoDig.Drop_Low_Durability_Tools$")] public bool Drop_Low_Durability_Tools = false; @@ -65,6 +64,8 @@ namespace MinecraftClient.ChatBots public void OnSettingUpdate() { + Durability_Limit = Math.Max(0, Durability_Limit); + if (Auto_Start_Delay >= 0) Auto_Start_Delay = Math.Max(0.1, Auto_Start_Delay); @@ -225,6 +226,102 @@ namespace MinecraftClient.ChatBots } } + private static int GetLegacyMaxDamage(ItemType itemType) + { + return itemType switch + { + ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or ItemType.WoodenSword or ItemType.WoodenHoe => 59, + ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or ItemType.StoneSword or ItemType.StoneHoe => 131, + ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or ItemType.IronSword or ItemType.IronHoe => 250, + ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or ItemType.GoldenSword or ItemType.GoldenHoe => 32, + ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or ItemType.DiamondSword or ItemType.DiamondHoe => 1561, + ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or ItemType.NetheriteSword or ItemType.NetheriteHoe => 2031, + ItemType.Shears => 238, + _ => 0 + }; + } + + private static int GetMaxDamage(Item item) + { + if (item.Components is not null) + { + var maxDamageComponent = item.Components.OfType().FirstOrDefault(); + if (maxDamageComponent is not null) + return maxDamageComponent.MaxDamage; + } + + return GetLegacyMaxDamage(item.Type); + } + + private static int GetRemainingDurability(Item item) + { + int maxDamage = GetMaxDamage(item); + return maxDamage > 0 ? maxDamage - item.Damage : int.MaxValue; + } + + private bool HasEnoughDurability(Item item) + { + return Config.Durability_Limit <= 0 || GetRemainingDurability(item) >= Config.Durability_Limit; + } + + private bool IsBelowDurabilityLimit(Item? item) + { + return item is not null && Config.Durability_Limit > 0 && GetRemainingDurability(item) < Config.Durability_Limit; + } + + private static bool IsRecommendedTool(Item? item, ItemType[] recommendedTools) + { + return item is not null && recommendedTools.Contains(item.Type); + } + + private bool SwapToolIntoHand(int sourceSlot, int handSlot) + { + return WindowAction(0, sourceSlot, WindowActionType.LeftClick) + && WindowAction(0, handSlot, WindowActionType.LeftClick) + && WindowAction(0, sourceSlot, WindowActionType.LeftClick); + } + + private bool EnsureSuitableTool(Material blockType) + { + if (!inventoryEnabled || !Config.Auto_Tool_Switch) + return true; + + ItemType[] recommendedTools = Material2Tool.GetCorrectToolForBlock(blockType); + if (recommendedTools.Length == 0) + return true; + + Container container = GetPlayerInventory(); + int handSlot = 36 + GetCurrentSlot(); + container.Items.TryGetValue(handSlot, out Item? currentTool); + + if (currentTool is not null && IsRecommendedTool(currentTool, recommendedTools) && HasEnoughDurability(currentTool)) + return true; + + foreach (ItemType recommendedTool in recommendedTools) + { + foreach ((int slot, Item item) in container.Items) + { + if (slot == handSlot || item.Type != recommendedTool || !HasEnoughDurability(item)) + continue; + + if (!SwapToolIntoHand(slot, handSlot)) + return false; + + LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_switch, item.GetTypeString(), slot)); + + if (Config.Drop_Low_Durability_Tools && IsBelowDurabilityLimit(currentTool) && + WindowAction(0, slot, WindowActionType.DropItemStack)) + { + LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_drop_low_durability, currentTool!.GetTypeString(), slot)); + } + + return true; + } + } + + return !IsBelowDurabilityLimit(currentTool); + } + public override void Update() { lock (stateLock) @@ -293,6 +390,9 @@ namespace MinecraftClient.ChatBots if (Config.Mode == Configs.ModeType.lookat || (Config.Mode == Configs.ModeType.both && Config._Locations.Contains(blockLoc))) { + if (!EnsureSuitableTool(block.Type)) + return false; + if (DigBlock(blockLoc, Direction.Down, lookAtBlock: false)) { currentDig = blockLoc; @@ -354,6 +454,9 @@ namespace MinecraftClient.ChatBots if (minDistance <= 6.0) { + if (!EnsureSuitableTool(targetBlock.Type)) + return false; + if (DigBlock(target, Direction.Down, lookAtBlock: true)) { currentDig = target; @@ -388,6 +491,9 @@ namespace MinecraftClient.ChatBots ((Config.List_Type == Configs.ListType.whitelist && Config.Blocks.Contains(block.Type)) || (Config.List_Type == Configs.ListType.blacklist && !Config.Blocks.Contains(block.Type)))) { + if (!EnsureSuitableTool(block.Type)) + return false; + if (DigBlock(blockLoc, Direction.Down, lookAtBlock: true)) { currentDig = blockLoc; diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 9711b86c..cf381561 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -62,6 +62,21 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$ChatBot.AutoFishing.Hook_Threshold$")] public double Hook_Threshold = 0.2; + [TomlInlineComment("$ChatBot.AutoFishing.Enable_Velocity_Detection$")] + public bool Enable_Velocity_Detection = true; + + [TomlInlineComment("$ChatBot.AutoFishing.Velocity_Hook_Threshold$")] + public double Velocity_Hook_Threshold = -0.2; + + [TomlInlineComment("$ChatBot.AutoFishing.Enable_Sound_Detection$")] + public bool Enable_Sound_Detection = true; + + [TomlInlineComment("$ChatBot.AutoFishing.Sound_Distance$")] + public double Sound_Distance = 5.0; + + [TomlInlineComment("$ChatBot.AutoFishing.Detection_Warmup$")] + public double Detection_Warmup = 1.0; + [TomlInlineComment("$ChatBot.AutoFishing.Log_Fish_Bobber$")] public bool Log_Fish_Bobber = false; @@ -97,6 +112,15 @@ namespace MinecraftClient.ChatBots if (Hook_Threshold < 0) Hook_Threshold = -Hook_Threshold; + + if (Velocity_Hook_Threshold > 0) + Velocity_Hook_Threshold = -Velocity_Hook_Threshold; + + if (Sound_Distance < 0) + Sound_Distance = -Sound_Distance; + + if (Detection_Warmup < 0) + Detection_Warmup = 0; } public struct LocationConfig @@ -171,6 +195,7 @@ namespace MinecraftClient.ChatBots private Entity? fishingBobber; private Location LastPos = Location.Zero; private DateTime CaughtTime = DateTime.Now; + private DateTime BobberSpawnTime = DateTime.MinValue; private int fishItemCounter = 15; private Dictionary fishItemCnt = new(); private Entity fishItem = new(-1, EntityType.Item, Location.Zero); @@ -464,6 +489,7 @@ namespace MinecraftClient.ChatBots fishingBobber = entity; LastPos = entity.Location; isFishing = true; + BobberSpawnTime = DateTime.Now; castTimeout = 24; counter = 0; @@ -500,7 +526,7 @@ namespace MinecraftClient.ChatBots public override void OnEntityMove(Entity entity) { if (isFishing && entity is not null && fishingBobber!.ID == entity.ID && - (state == FishingState.WaitingFishToBite || state == FishingState.WaitingFishingBobber)) + state == FishingState.WaitingFishToBite) { Location Pos = entity.Location; double Dx = LastPos.X - Pos.X; @@ -515,13 +541,7 @@ namespace MinecraftClient.ChatBots Math.Abs(Dz) < Math.Abs(Config.Stationary_Threshold) && Math.Abs(Dy) > Math.Abs(Config.Hook_Threshold)) { - // prevent triggering multiple time - if ((DateTime.Now - CaughtTime).TotalSeconds > 1) - { - isFishing = false; - CaughtTime = DateTime.Now; - OnCaughtFish(); - } + TryCatchFish(); } } } @@ -540,6 +560,38 @@ namespace MinecraftClient.ChatBots } } + public override void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) + { + if (!Config.Enable_Velocity_Detection || !CanUseAdvancedDetection()) + return; + + if (fishingBobber is null || entity.ID != fishingBobber.ID) + return; + + if (velocityY <= Config.Velocity_Hook_Threshold) + TryCatchFish(); + } + + public override void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + Entity? sourceEntity) + { + if (!Config.Enable_Sound_Detection || !CanUseAdvancedDetection()) + return; + + if (!IsFishingBobberSplashSound(soundName)) + return; + + Location? soundLocation = location; + if (soundLocation is null && sourceEntity is not null) + soundLocation = sourceEntity.Location; + + if (soundLocation is null || fishingBobber is null) + return; + + if (soundLocation.Value.Distance(fishingBobber.Location) <= Config.Sound_Distance) + TryCatchFish(); + } + public override void AfterGameJoined() { StartFishing(); @@ -562,10 +614,42 @@ namespace MinecraftClient.ChatBots fishingBobber = null; LastPos = Location.Zero; CaughtTime = DateTime.Now; + BobberSpawnTime = DateTime.MinValue; return base.OnDisconnect(reason, message); } + private bool CanUseAdvancedDetection() + { + if (!isFishing || fishingBobber is null || state != FishingState.WaitingFishToBite) + return false; + + return (DateTime.Now - BobberSpawnTime).TotalSeconds >= Config.Detection_Warmup; + } + + private void TryCatchFish() + { + if (!CanUseAdvancedDetection()) + return; + + // Prevent repeated catches from multiple packets of the same bite. + if ((DateTime.Now - CaughtTime).TotalSeconds <= 1) + return; + + isFishing = false; + CaughtTime = DateTime.Now; + OnCaughtFish(); + } + + private static bool IsFishingBobberSplashSound(string? soundName) + { + return string.Equals(soundName, "minecraft:entity.fishing_bobber.splash", + StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "entity.fishing_bobber.splash", StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "minecraft:entity.bobber.splash", StringComparison.OrdinalIgnoreCase) + || string.Equals(soundName, "entity.bobber.splash", StringComparison.OrdinalIgnoreCase); + } + /// /// Called when detected a fish is caught /// diff --git a/MinecraftClient/ChatBots/AutoRelog.cs b/MinecraftClient/ChatBots/AutoRelog.cs index 316c5ab5..8a42a26c 100644 --- a/MinecraftClient/ChatBots/AutoRelog.cs +++ b/MinecraftClient/ChatBots/AutoRelog.cs @@ -1,4 +1,4 @@ -using System; +using System; using MinecraftClient.Scripting; using Tomlet.Attributes; @@ -95,6 +95,11 @@ namespace MinecraftClient.ChatBots _Initialize(); } + public override void AfterGameJoined() + { + Configs._BotRecoAttempts = 0; + } + private void _Initialize() { McClient.ReconnectionAttemptsLeft = Config.Retries; @@ -144,10 +149,17 @@ namespace MinecraftClient.ChatBots { double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min; LogDebugToConsole(string.Format(string.IsNullOrEmpty(msg) ? Translations.bot_autoRelog_reconnect_always : Translations.bot_autoRelog_reconnect, msg)); - - // TODO: Change this translation string to add the retries left text - LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay) + $" ({Config.Retries - Configs._BotRecoAttempts} retries left)"); - ReconnectToTheServer(Config.Retries - Configs._BotRecoAttempts, (int)Math.Floor(delay), true); + + int retriesLeft = Config.Retries - Configs._BotRecoAttempts; + if (retriesLeft < 0) + retriesLeft = 0; + + string retriesDisplay = Config.Retries == int.MaxValue + ? Translations.bot_autoRelog_retries_unlimited + : retriesLeft.ToString(); + + LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay)); + ReconnectToTheServer(retriesLeft, (int)Math.Floor(delay), true); } public static bool OnDisconnectStatic(DisconnectReason reason, string message) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index fa13f84e..3938ab6a 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -1,8 +1,11 @@ -using System; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Brigadier.NET.Builder; using DSharpPlus; @@ -34,6 +37,9 @@ namespace MinecraftClient.ChatBots private DiscordChannel? discordChannel; private BridgeDirection bridgeDirection = BridgeDirection.Both; + private readonly ConcurrentQueue aggregationBuffer = new(); + private Timer? aggregationTimer; + public static Configs Config = new(); [TomlDoNotInlineObject] @@ -62,6 +68,12 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$ChatBot.DiscordBridge.AllowOtherBotMessages$")] public bool Allow_Other_Bot_Messages = false; + [TomlInlineComment("$ChatBot.DiscordBridge.RelayAllMessages$")] + public bool Relay_All_Messages = false; + + [TomlInlineComment("$ChatBot.DiscordBridge.MessageAggregationInterval$")] + public double Message_Aggregation_Interval = 3.0; + [TomlPrecedingComment("$ChatBot.DiscordBridge.Formats$")] public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; public string PublicMessageFormat = "{username}: {message}"; @@ -70,6 +82,8 @@ namespace MinecraftClient.ChatBots public void OnSettingUpdate() { Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout; + if (Message_Aggregation_Interval < 0) + Message_Aggregation_Interval = 0; } } @@ -100,6 +114,12 @@ namespace MinecraftClient.ChatBots .Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName))) ); + if (Config.Message_Aggregation_Interval > 0) + { + var intervalMs = (int)(Config.Message_Aggregation_Interval * 1000); + aggregationTimer = new Timer(_ => FlushAggregationBuffer(), null, intervalMs, intervalMs); + } + Task.Run(async () => await MainAsync()); } @@ -107,6 +127,7 @@ namespace MinecraftClient.ChatBots { McClient.dispatcher.Unregister(CommandName); McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName); + StopAggregation(); Disconnect(); } @@ -147,6 +168,40 @@ namespace MinecraftClient.ChatBots return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.bot_DiscordBridge_direction, bridgeName)); } + private void FlushAggregationBuffer() + { + if (aggregationBuffer.IsEmpty || !CanSendMessages()) + return; + + var sb = new StringBuilder(); + while (aggregationBuffer.TryDequeue(out var line)) + { + if (sb.Length + line.Length + 1 > 1900) + { + SendMessage(sb.ToString()); + sb.Clear(); + } + + if (sb.Length > 0) + sb.AppendLine(); + sb.Append(line); + } + + if (sb.Length > 0) + SendMessage(sb.ToString()); + } + + private void StopAggregation() + { + if (aggregationTimer is not null) + { + aggregationTimer.Dispose(); + aggregationTimer = null; + } + + FlushAggregationBuffer(); + } + ~DiscordBridge() { Disconnect(); @@ -188,7 +243,6 @@ namespace MinecraftClient.ChatBots text = GetVerbatim(text).Trim(); - // Stop the crash when an empty text is recived somehow if (string.IsNullOrEmpty(text)) return; @@ -205,7 +259,10 @@ namespace MinecraftClient.ChatBots message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim(); teleportRequest = true; } - else message = text; + else if (Config.Relay_All_Messages) + message = text; + else + return; if (teleportRequest) { @@ -223,7 +280,13 @@ namespace MinecraftClient.ChatBots SendMessage(messageBuilder); return; } - else SendMessage(GetDiscordText(message)); + + string discordText = GetDiscordText(message); + + if (Config.Message_Aggregation_Interval > 0) + aggregationBuffer.Enqueue(discordText); + else + SendMessage(discordText); } /// diff --git a/MinecraftClient/Commands/AchievementCommand.cs b/MinecraftClient/Commands/AchievementCommand.cs new file mode 100644 index 00000000..ee99c4d7 --- /dev/null +++ b/MinecraftClient/Commands/AchievementCommand.cs @@ -0,0 +1,106 @@ +using System.Linq; +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class AchievementCommand : Command + { + public override string CmdName => "achievement"; + public override string CmdUsage => "achievement "; + public override string CmdDesc => Translations.cmd_achievement_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + .Then(l => l.Literal("list") + .Executes(r => GetUsage(r.Source, "list"))) + .Then(l => l.Literal("locked") + .Executes(r => GetUsage(r.Source, "locked"))) + .Then(l => l.Literal("unlocked") + .Executes(r => GetUsage(r.Source, "unlocked"))) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => ListAchievements(r.Source, null)) + .Then(l => l.Literal("list") + .Executes(r => ListAchievements(r.Source, null))) + .Then(l => l.Literal("locked") + .Executes(r => ListAchievements(r.Source, false))) + .Then(l => l.Literal("unlocked") + .Executes(r => ListAchievements(r.Source, true))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format + "list" => GetCmdDescTranslated(), + "locked" => GetCmdDescTranslated(), + "unlocked" => GetCmdDescTranslated(), + _ => GetCmdDescTranslated(), +#pragma warning restore format + }); + } + + /// null = all, true = unlocked only, false = locked only + private static int ListAchievements(CmdResult r, bool? completed) + { + McClient handler = CmdResult.currentHandler!; + + Achievement[] items = completed switch + { + true => handler.GetUnlockedAchievements(), + false => handler.GetLockedAchievements(), + null => handler.GetAchievements() + }; + + if (items.Length == 0) + { + string msg = completed switch + { + true => Translations.cmd_achievement_none_unlocked, + false => Translations.cmd_achievement_none_locked, + _ => Translations.cmd_achievement_none + }; + return r.SetAndReturn(CmdResult.Status.Done, msg); + } + + string header = completed switch + { + true => Translations.cmd_achievement_header_unlocked, + false => Translations.cmd_achievement_header_locked, + _ => Translations.cmd_achievement_header + }; + + StringBuilder sb = new(); + sb.AppendLine(header); + + foreach (Achievement a in items.OrderBy(static a => a.Id)) + { + string status = a.IsCompleted + ? Translations.cmd_achievement_done + : Translations.cmd_achievement_todo; + + string display = a.Title is not null + ? string.Format(Translations.cmd_achievement_entry_titled, status, a.Title, a.Id, a.Type) + : string.Format(Translations.cmd_achievement_entry, status, a.Id, a.Type); + + sb.AppendLine(display); + } + + handler.Log.Info(sb.ToString().TrimEnd()); + return r.SetAndReturn(CmdResult.Status.Done); + } + } +} diff --git a/MinecraftClient/Commands/Minimap.cs b/MinecraftClient/Commands/Minimap.cs new file mode 100644 index 00000000..0b6ca00f --- /dev/null +++ b/MinecraftClient/Commands/Minimap.cs @@ -0,0 +1,284 @@ +using System; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using MinecraftClient.Tui; +using Avalonia.Threading; +using static MinecraftClient.CommandHandler.CmdResult; + +namespace MinecraftClient.Commands +{ + class Minimap : Command + { + public override string CmdName => "minimap"; + public override string CmdUsage => "minimap [on|off] | minimap zoom [in|out|<1-16>] | minimap names [players|hostile|neutral|passive] [on|off] | minimap names [all_on|all_off] | minimap position [top_left|top_right|center|bottom_left|bottom_right] | minimap cave [auto|on|off]"; + public override string CmdDesc => Translations.cmd_minimap_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => DoToggle(r.Source)) + .Then(l => l.Literal("on") + .Executes(r => DoOn(r.Source))) + .Then(l => l.Literal("off") + .Executes(r => DoOff(r.Source))) + .Then(l => l.Literal("zoom") + .Executes(r => DoZoomInfo(r.Source)) + .Then(l => l.Literal("in") + .Executes(r => DoZoomIn(r.Source))) + .Then(l => l.Literal("out") + .Executes(r => DoZoomOut(r.Source))) + .Then(l => l.Argument("level", Arguments.Integer(MinimapControl.MinZoom, MinimapControl.MaxZoom)) + .Executes(r => DoZoomSet(r.Source, Arguments.GetInteger(r, "level"))))) + .Then(l => l.Literal("names") + .Executes(r => DoNamesInfo(r.Source)) + .Then(l => l.Literal("all_on") + .Executes(r => DoNamesAll(r.Source, true))) + .Then(l => l.Literal("all_off") + .Executes(r => DoNamesAll(r.Source, false))) + .Then(l => l.Literal("players") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Player)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, false)))) + .Then(l => l.Literal("hostile") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Hostile)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, false)))) + .Then(l => l.Literal("neutral") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Neutral)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, false)))) + .Then(l => l.Literal("passive") + .Executes(r => DoNamesCatInfo(r.Source, MobCategory.Passive)) + .Then(l => l.Literal("on") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, true))) + .Then(l => l.Literal("off") + .Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, false))))) + .Then(l => l.Literal("position") + .Executes(r => DoPositionInfo(r.Source)) + .Then(l => l.Literal("top_left") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.top_left))) + .Then(l => l.Literal("top_right") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.top_right))) + .Then(l => l.Literal("center") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.center))) + .Then(l => l.Literal("bottom_left") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_left))) + .Then(l => l.Literal("bottom_right") + .Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_right)))) + .Then(l => l.Literal("cave") + .Executes(r => DoCaveInfo(r.Source)) + .Then(l => l.Literal("auto") + .Executes(r => DoCaveSet(r.Source, CaveModeOption.auto))) + .Then(l => l.Literal("on") + .Executes(r => DoCaveSet(r.Source, CaveModeOption.on))) + .Then(l => l.Literal("off") + .Executes(r => DoCaveSet(r.Source, CaveModeOption.off)))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help")?.GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string _) => + r.SetAndReturn(GetCmdDescTranslated()); + + private static MainTuiView? GetTuiView(CmdResult r) + { + if (ConsoleIO.Backend is not TuiConsoleBackend) + { + r.SetAndReturn(Status.Fail, Translations.cmd_minimap_tui_only); + return null; + } + return TuiConsoleBackend.Instance?.GetView(); + } + + private static int DoToggle(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + bool wasVisible = view.IsMinimapVisible; + Dispatcher.UIThread.Post(() => view.ToggleMinimap()); + string msg = wasVisible + ? Translations.cmd_minimap_disabled + : Translations.cmd_minimap_enabled; + return r.SetAndReturn(Status.Done, msg); + } + + private static int DoOn(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.ShowMinimap()); + return r.SetAndReturn(Status.Done, Translations.cmd_minimap_enabled); + } + + private static int DoOff(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.HideMinimap()); + return r.SetAndReturn(Status.Done, Translations.cmd_minimap_disabled); + } + + private static int DoZoomInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + int current = view.GetMinimapZoom(); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_zoom_current, current, MinimapControl.MaxZoom)); + } + + private static int DoZoomIn(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + int newLevel = Math.Max(view.GetMinimapZoom() - 1, MinimapControl.MinZoom); + Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel)); + return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel)); + } + + private static int DoZoomOut(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + int newLevel = Math.Min(view.GetMinimapZoom() + 1, MinimapControl.MaxZoom); + Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel)); + return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel)); + } + + private static int DoZoomSet(CmdResult r, int level) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.SetMinimapZoom(level)); + return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, level)); + } + + private static int DoNamesInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var nc = view.GetMinimapNameConfig(); + string status = string.Format(Translations.cmd_minimap_names_status, + BoolStr(nc.Players), BoolStr(nc.Hostile), BoolStr(nc.Neutral), BoolStr(nc.Passive)); + return r.SetAndReturn(Status.Done, status); + } + + private static int DoNamesAll(CmdResult r, bool on) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => + { + view.GetMinimapNameConfig().SetAll(on); + view.SyncMinimapNameConfig(); + }); + string msg = on ? Translations.cmd_minimap_names_all_on : Translations.cmd_minimap_names_all_off; + return r.SetAndReturn(Status.Done, msg); + } + + private static int DoNamesCatInfo(CmdResult r, MobCategory cat) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var nc = view.GetMinimapNameConfig(); + bool val = cat switch + { + MobCategory.Player => nc.Players, + MobCategory.Hostile => nc.Hostile, + MobCategory.Neutral => nc.Neutral, + MobCategory.Passive => nc.Passive, + _ => false, + }; + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_names_cat, cat, BoolStr(val))); + } + + private static int DoNamesCatSet(CmdResult r, MobCategory cat, bool on) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => + { + var nc = view.GetMinimapNameConfig(); + switch (cat) + { + case MobCategory.Player: nc.Players = on; break; + case MobCategory.Hostile: nc.Hostile = on; break; + case MobCategory.Neutral: nc.Neutral = on; break; + case MobCategory.Passive: nc.Passive = on; break; + } + view.SyncMinimapNameConfig(); + }); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_names_cat_set, cat, BoolStr(on))); + } + + private static int DoPositionInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var pos = view.GetMinimapPosition(); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_position_current, pos)); + } + + private static int DoPositionSet(CmdResult r, MinimapPosition pos) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.SetMinimapPosition(pos)); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_position_set, pos)); + } + + private static int DoCaveInfo(CmdResult r) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + var mode = view.GetMinimapCaveMode(); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_cave_current, mode)); + } + + private static int DoCaveSet(CmdResult r, CaveModeOption mode) + { + var view = GetTuiView(r); + if (view is null) return (int)r.status; + + Dispatcher.UIThread.Post(() => view.SetMinimapCaveMode(mode)); + return r.SetAndReturn(Status.Done, + string.Format(Translations.cmd_minimap_cave_set, mode)); + } + + private static string BoolStr(bool v) => v ? "ON" : "OFF"; + } +} diff --git a/MinecraftClient/Commands/RecipeBook.cs b/MinecraftClient/Commands/RecipeBook.cs new file mode 100644 index 00000000..4cf0d873 --- /dev/null +++ b/MinecraftClient/Commands/RecipeBook.cs @@ -0,0 +1,98 @@ +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; + +namespace MinecraftClient.Commands +{ + public class RecipeBook : Command + { + public override string CmdName => "recipebook"; + public override string CmdUsage => "recipebook [recipe id]"; + public override string CmdDesc => Translations.cmd_recipebook_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + .Then(l => l.Literal("list") + .Executes(r => GetUsage(r.Source, "list"))) + .Then(l => l.Literal("craft") + .Executes(r => GetUsage(r.Source, "craft"))) + .Then(l => l.Literal("craftall") + .Executes(r => GetUsage(r.Source, "craftall"))) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Then(l => l.Literal("list") + .Executes(r => ListRecipes(r.Source))) + .Then(l => l.Literal("craft") + .Then(l => l.Argument("RecipeId", Arguments.String()) + .Executes(r => CraftRecipe(r.Source, Arguments.GetString(r, "RecipeId"), makeAll: false)))) + .Then(l => l.Literal("craftall") + .Then(l => l.Argument("RecipeId", Arguments.String()) + .Executes(r => CraftRecipe(r.Source, Arguments.GetString(r, "RecipeId"), makeAll: true)))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + "list" => GetCmdDescTranslated(), + "craft" => GetCmdDescTranslated(), + "craftall" => GetCmdDescTranslated(), + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private int ListRecipes(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetInventoryEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedInventory); + + RecipeBookRecipeEntry[] recipes = handler.GetUnlockedRecipes(); + if (recipes.Length == 0) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_recipes); + + StringBuilder response = new(); + response.AppendLine(Translations.cmd_recipebook_list); + foreach (RecipeBookRecipeEntry recipe in recipes) + response.AppendLine("- " + recipe.DisplayText); + + handler.Log.Info(response.ToString().TrimEnd()); + return r.SetAndReturn(CmdResult.Status.Done); + } + + private int CraftRecipe(CmdResult r, string recipeId, bool makeAll) + { + McClient handler = CmdResult.currentHandler!; + if (!handler.GetInventoryEnabled()) + return r.SetAndReturn(CmdResult.Status.FailNeedInventory); + + if (string.IsNullOrWhiteSpace(recipeId)) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_recipe_id_empty); + + if (handler.GetProtocolVersion() < Protocol.Handlers.Protocol18Handler.MC_1_13_Version) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_unsupported); + + if (handler.GetActiveRecipeBookInventory() is null) + return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_active_inventory); + + string normalizedRecipeId = McClient.NormalizeRecipeArgument(recipeId, handler.GetProtocolVersion()); + string successMessage = string.Format(makeAll ? Translations.cmd_recipebook_craftall_sent : Translations.cmd_recipebook_craft_sent, normalizedRecipeId); + + return handler.SendPlaceRecipe(recipeId, makeAll) + ? r.SetAndReturn(CmdResult.Status.Done, successMessage) + : r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, normalizedRecipeId)); + } + } +} diff --git a/MinecraftClient/Commands/Teams.cs b/MinecraftClient/Commands/Teams.cs new file mode 100644 index 00000000..af7a06d1 --- /dev/null +++ b/MinecraftClient/Commands/Teams.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Commands +{ + public class Teams : Command + { + public override string CmdName => "teams"; + public override string CmdUsage => "teams"; + public override string CmdDesc => Translations.cmd_teams_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source, string.Empty)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => DoListTeams(r.Source)) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source, string.Empty)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r, string? cmd) + { + return r.SetAndReturn(cmd switch + { +#pragma warning disable format // @formatter:off + _ => GetCmdDescTranslated(), +#pragma warning restore format // @formatter:on + }); + } + + private static int DoListTeams(CmdResult r) + { + McClient handler = CmdResult.currentHandler!; + Dictionary snapshot = handler.GetTeams(); + + if (snapshot.Count == 0) + return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_teams_no_teams); + + var sb = new StringBuilder(); + foreach (var team in snapshot.Values.OrderBy(static t => t.Name, StringComparer.Ordinal)) + { + sb.AppendLine(string.Format(Translations.cmd_teams_team_header, + team.Name, + team.DisplayName, + team.Color, + team.Prefix, + team.Suffix, + team.NameTagVisibility, + team.CollisionRule, + team.AllowFriendlyFire, + team.SeeFriendlyInvisibles)); + + if (team.Members.Count == 0) + sb.AppendLine(Translations.cmd_teams_team_no_members); + else + sb.AppendLine(string.Format(Translations.cmd_teams_team_members, + team.Members.Count, + string.Join(", ", team.Members.OrderBy(static m => m, StringComparer.OrdinalIgnoreCase)))); + } + + return r.SetAndReturn(CmdResult.Status.Done, sb.ToString().TrimEnd()); + } + } +} diff --git a/MinecraftClient/Commands/Tryout.cs b/MinecraftClient/Commands/Tryout.cs new file mode 100644 index 00000000..8ae0829b --- /dev/null +++ b/MinecraftClient/Commands/Tryout.cs @@ -0,0 +1,63 @@ +using Brigadier.NET; +using Brigadier.NET.Builder; +using MinecraftClient.CommandHandler; +using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig; + +namespace MinecraftClient.Commands +{ + public class Tryout : Command + { + public override string CmdName => "tryout"; + public override string CmdUsage => "tryout [list|tui]"; + public override string CmdDesc => Translations.cmd_tryout_desc; + + public override void RegisterCommand(CommandDispatcher dispatcher) + { + dispatcher.Register(l => l.Literal("help") + .Then(l => l.Literal(CmdName) + .Executes(r => GetUsage(r.Source)) + ) + ); + + dispatcher.Register(l => l.Literal(CmdName) + .Executes(r => ListTryouts(r.Source)) + .Then(l => l.Literal("list") + .Executes(r => ListTryouts(r.Source))) + .Then(l => l.Literal("tui") + .Executes(r => EnableTuiMode(r.Source))) + .Then(l => l.Literal("_help") + .Executes(r => GetUsage(r.Source)) + .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) + ); + } + + private int GetUsage(CmdResult r) + { + return r.SetAndReturn(GetCmdDescTranslated()); + } + + private int ListTryouts(CmdResult r) + { + return r.SetAndReturn(string.Join('\n', + GetCmdDescTranslated(), + string.Empty, + Translations.cmd_tryout_list_header, + $" - {Translations.cmd_tryout_list_tui}")); + } + + private int EnableTuiMode(CmdResult r) + { + var previousMode = Settings.Config.Console.General.ConsoleMode; + if (previousMode == ConsoleModeType.tui) + { + return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_tryout_tui_already_enabled); + } + + Settings.Config.Console.General.ConsoleMode = ConsoleModeType.tui; + Program.WriteBackSettings(); + + return r.SetAndReturn(CmdResult.Status.Done, + string.Format(Translations.cmd_tryout_tui_enabled, previousMode, ConsoleModeType.tui)); + } + } +} diff --git a/MinecraftClient/Commands/Useblock.cs b/MinecraftClient/Commands/Useblock.cs index 2df6a92f..7e482ba4 100644 --- a/MinecraftClient/Commands/Useblock.cs +++ b/MinecraftClient/Commands/Useblock.cs @@ -1,6 +1,7 @@ using Brigadier.NET; using Brigadier.NET.Builder; using MinecraftClient.CommandHandler; +using MinecraftClient.Inventory; using MinecraftClient.Mapping; using static MinecraftClient.CommandHandler.CmdResult; @@ -9,7 +10,7 @@ namespace MinecraftClient.Commands class Useblock : Command { public override string CmdName { get { return "useblock"; } } - public override string CmdUsage { get { return "useblock "; } } + public override string CmdUsage { get { return "useblock [mainhand|offhand]"; } } public override string CmdDesc { get { return Translations.cmd_useblock_desc; } } public override void RegisterCommand(CommandDispatcher dispatcher) @@ -22,7 +23,11 @@ namespace MinecraftClient.Commands dispatcher.Register(l => l.Literal(CmdName) .Then(l => l.Argument("Location", MccArguments.Location()) - .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location")))) + .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.MainHand)) + .Then(l => l.Literal("mainhand") + .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.MainHand))) + .Then(l => l.Literal("offhand") + .Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.OffHand)))) .Then(l => l.Literal("_help") .Executes(r => GetUsage(r.Source, string.Empty)) .Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName))) @@ -39,7 +44,7 @@ namespace MinecraftClient.Commands }); } - private int UseBlockAtLocation(CmdResult r, Location block) + private int UseBlockAtLocation(CmdResult r, Location block, Hand hand) { McClient handler = CmdResult.currentHandler!; if (!handler.GetTerrainEnabled()) @@ -48,7 +53,7 @@ namespace MinecraftClient.Commands Location current = handler.GetCurrentLocation(); block = block.ToAbsolute(current).ToFloor(); Location blockCenter = block.ToCenter(); - bool res = handler.PlaceBlock(block, Direction.Down, lookAtBlock: true); + bool res = handler.PlaceBlock(block, Direction.Down, hand, lookAtBlock: true); return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res); } } diff --git a/MinecraftClient/ConsoleIO.cs b/MinecraftClient/ConsoleIO.cs index 485d5a90..8a77aaee 100644 --- a/MinecraftClient/ConsoleIO.cs +++ b/MinecraftClient/ConsoleIO.cs @@ -104,7 +104,7 @@ namespace MinecraftClient /// public static void WriteLine(string line) { - if (BasicIO) + if (BasicIO || Backend is null) Console.WriteLine(line); else Backend.WriteLine(line); @@ -137,7 +137,7 @@ namespace MinecraftClient { str = str.Replace('\n', ' '); } - if (BasicIO) + if (BasicIO || Backend is null) { if (BasicIO_NoColor) { diff --git a/MinecraftClient/Json.cs b/MinecraftClient/Json.cs index da3aa838..08f5b24c 100644 --- a/MinecraftClient/Json.cs +++ b/MinecraftClient/Json.cs @@ -1,3 +1,4 @@ +using System; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -23,10 +24,46 @@ public static class Json public static JsonNode? ParseJson(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; + ReadOnlySpan text = json.AsSpan().TrimStart(); + if (!LooksLikeJson(text)) + return JsonValue.Create(json); + try { return JsonNode.Parse(json); } catch (JsonException) { return JsonValue.Create(json); } } + private static bool LooksLikeJson(ReadOnlySpan text) + { + if (text.IsEmpty) + return false; + + return text[0] switch + { + '{' or '"' => true, + '[' => LooksLikeJsonArray(text[1..]), + '-' => text.Length > 1 && char.IsAsciiDigit(text[1]), + >= '0' and <= '9' => true, + 't' or 'f' or 'n' => true, + _ => false + }; + } + + private static bool LooksLikeJsonArray(ReadOnlySpan text) + { + text = text.TrimStart(); + if (text.IsEmpty) + return false; + + return text[0] switch + { + ']' or '{' or '[' or '"' => true, + '-' => text.Length > 1 && char.IsAsciiDigit(text[1]), + >= '0' and <= '9' => true, + 't' or 'f' or 'n' => true, + _ => false + }; + } + /// /// Escape a string for embedding inside a JSON string literal. /// Uses System.Text.Json serialization and strips the surrounding quotes. @@ -52,4 +89,4 @@ public static class JsonNodeExtensions JsonValue val when val.TryGetValue(out var s) => s, _ => node.ToJsonString() }; -} \ No newline at end of file +} diff --git a/MinecraftClient/LegacyAchievementCatalog.cs b/MinecraftClient/LegacyAchievementCatalog.cs new file mode 100644 index 00000000..bce17f8d --- /dev/null +++ b/MinecraftClient/LegacyAchievementCatalog.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace MinecraftClient +{ + internal static class LegacyAchievementCatalog + { + public static IReadOnlyList Ids { get; } = + [ + "achievement.openInventory", + "achievement.mineWood", + "achievement.buildWorkBench", + "achievement.buildPickaxe", + "achievement.buildFurnace", + "achievement.acquireIron", + "achievement.buildHoe", + "achievement.makeBread", + "achievement.bakeCake", + "achievement.buildBetterPickaxe", + "achievement.cookFish", + "achievement.onARail", + "achievement.buildSword", + "achievement.killEnemy", + "achievement.killCow", + "achievement.flyPig", + "achievement.snipeSkeleton", + "achievement.diamonds", + "achievement.diamondsToYou", + "achievement.portal", + "achievement.ghast", + "achievement.blazeRod", + "achievement.potion", + "achievement.theEnd", + "achievement.theEnd2", + "achievement.enchantments", + "achievement.overkill", + "achievement.bookcase", + "achievement.breedCow", + "achievement.spawnWither", + "achievement.killWither", + "achievement.fullBeacon", + "achievement.exploreAllBiomes", + "achievement.overpowered" + ]; + + private static readonly HashSet s_idSet = new(Ids, StringComparer.Ordinal); + + public static bool Contains(string id) + { + return s_idSet.Contains(id); + } + } +} diff --git a/MinecraftClient/Mapping/BlockHardness.cs b/MinecraftClient/Mapping/BlockHardness.cs new file mode 100644 index 00000000..84cd8f92 --- /dev/null +++ b/MinecraftClient/Mapping/BlockHardness.cs @@ -0,0 +1,1326 @@ +using System.Collections.Frozen; +using System.Collections.Generic; + +namespace MinecraftClient.Mapping +{ + /// + /// Provides block hardness values and tool requirement data for mining calculations. + /// Data extracted from Minecraft 1.21.11 decompiled source (Blocks.java). + /// + public static class BlockHardness + { + /// + /// Default hardness for blocks not in the table (assumes stone-like). + /// + public const float DefaultHardness = 1.5f; + + /// + /// Get the hardness value for a block material. + /// Returns -1 for unbreakable blocks, 0 for instant-break blocks. + /// + public static float GetHardness(Material material) + { + if (HardnessTable.TryGetValue(material, out float hardness)) + return hardness; + return DefaultHardness; + } + + /// + /// Check whether a block requires the correct tool to get drops + /// (and uses the 100 divisor instead of 30 when mined without the correct tool). + /// + public static bool RequiresCorrectTool(Material material) + { + return RequiresCorrectToolSet.Contains(material); + } + + private static readonly FrozenDictionary HardnessTable = new Dictionary + { + // Hardness -1.0: 15 blocks + { Material.Barrier, -1.0f }, + { Material.Bedrock, -1.0f }, + { Material.ChainCommandBlock, -1.0f }, + { Material.CommandBlock, -1.0f }, + { Material.EndGateway, -1.0f }, + { Material.EndPortal, -1.0f }, + { Material.EndPortalFrame, -1.0f }, + { Material.Jigsaw, -1.0f }, + { Material.Light, -1.0f }, + { Material.MovingPiston, -1.0f }, + { Material.NetherPortal, -1.0f }, + { Material.RepeatingCommandBlock, -1.0f }, + { Material.StructureBlock, -1.0f }, + { Material.TestBlock, -1.0f }, + { Material.TestInstanceBlock, -1.0f }, + // Hardness 0.0: 443 blocks + { Material.AcaciaButton, 0.0f }, + { Material.AcaciaLeaves, 0.0f }, + { Material.AcaciaLog, 0.0f }, + { Material.AcaciaSapling, 0.0f }, + { Material.Air, 0.0f }, + { Material.Allium, 0.0f }, + { Material.AndesiteSlab, 0.0f }, + { Material.AndesiteWall, 0.0f }, + { Material.Azalea, 0.0f }, + { Material.AzaleaLeaves, 0.0f }, + { Material.AzureBluet, 0.0f }, + { Material.Bamboo, 0.0f }, + { Material.BambooBlock, 0.0f }, + { Material.BambooButton, 0.0f }, + { Material.BambooSapling, 0.0f }, + { Material.Beetroots, 0.0f }, + { Material.BirchButton, 0.0f }, + { Material.BirchLeaves, 0.0f }, + { Material.BirchLog, 0.0f }, + { Material.BirchSapling, 0.0f }, + { Material.BlackCandle, 0.0f }, + { Material.BlackCandleCake, 0.0f }, + { Material.BlackShulkerBox, 0.0f }, + { Material.BlackstoneWall, 0.0f }, + { Material.BlueCandle, 0.0f }, + { Material.BlueCandleCake, 0.0f }, + { Material.BlueOrchid, 0.0f }, + { Material.BlueShulkerBox, 0.0f }, + { Material.BrainCoral, 0.0f }, + { Material.BrainCoralFan, 0.0f }, + { Material.BrainCoralWallFan, 0.0f }, + { Material.BrickWall, 0.0f }, + { Material.BrownCandle, 0.0f }, + { Material.BrownCandleCake, 0.0f }, + { Material.BrownMushroom, 0.0f }, + { Material.BrownShulkerBox, 0.0f }, + { Material.BubbleColumn, 0.0f }, + { Material.BubbleCoral, 0.0f }, + { Material.BubbleCoralFan, 0.0f }, + { Material.BubbleCoralWallFan, 0.0f }, + { Material.Bush, 0.0f }, + { Material.CactusFlower, 0.0f }, + { Material.CalibratedSculkSensor, 0.0f }, + { Material.Candle, 0.0f }, + { Material.CandleCake, 0.0f }, + { Material.Carrots, 0.0f }, + { Material.CaveAir, 0.0f }, + { Material.CaveVines, 0.0f }, + { Material.CaveVinesPlant, 0.0f }, + { Material.CherryButton, 0.0f }, + { Material.CherryLog, 0.0f }, + { Material.CherrySapling, 0.0f }, + { Material.ChiseledCopper, 0.0f }, + { Material.ChiseledDeepslate, 0.0f }, + { Material.ChiseledTuff, 0.0f }, + { Material.ChiseledTuffBricks, 0.0f }, + { Material.ClosedEyeblossom, 0.0f }, + { Material.CobbledDeepslateSlab, 0.0f }, + { Material.CobbledDeepslateWall, 0.0f }, + { Material.CobblestoneWall, 0.0f }, + { Material.Comparator, 0.0f }, + { Material.CopperOre, 0.0f }, + { Material.CopperTorch, 0.0f }, + { Material.CopperWallTorch, 0.0f }, + { Material.Cornflower, 0.0f }, + { Material.CrackedDeepslateBricks, 0.0f }, + { Material.CrackedDeepslateTiles, 0.0f }, + { Material.CrackedPolishedBlackstoneBricks, 0.0f }, + { Material.CrimsonButton, 0.0f }, + { Material.CrimsonFungus, 0.0f }, + { Material.CrimsonRoots, 0.0f }, + { Material.CrimsonStem, 0.0f }, + { Material.CutCopper, 0.0f }, + { Material.CutCopperSlab, 0.0f }, + { Material.CutCopperStairs, 0.0f }, + { Material.CyanCandle, 0.0f }, + { Material.CyanCandleCake, 0.0f }, + { Material.CyanShulkerBox, 0.0f }, + { Material.Dandelion, 0.0f }, + { Material.DarkOakButton, 0.0f }, + { Material.DarkOakLeaves, 0.0f }, + { Material.DarkOakLog, 0.0f }, + { Material.DarkOakSapling, 0.0f }, + { Material.DeadBrainCoral, 0.0f }, + { Material.DeadBrainCoralFan, 0.0f }, + { Material.DeadBrainCoralWallFan, 0.0f }, + { Material.DeadBubbleCoral, 0.0f }, + { Material.DeadBubbleCoralFan, 0.0f }, + { Material.DeadBubbleCoralWallFan, 0.0f }, + { Material.DeadBush, 0.0f }, + { Material.DeadFireCoral, 0.0f }, + { Material.DeadFireCoralFan, 0.0f }, + { Material.DeadFireCoralWallFan, 0.0f }, + { Material.DeadHornCoral, 0.0f }, + { Material.DeadHornCoralFan, 0.0f }, + { Material.DeadHornCoralWallFan, 0.0f }, + { Material.DeadTubeCoral, 0.0f }, + { Material.DeadTubeCoralFan, 0.0f }, + { Material.DeadTubeCoralWallFan, 0.0f }, + { Material.DecoratedPot, 0.0f }, + { Material.DeepslateBrickSlab, 0.0f }, + { Material.DeepslateBrickWall, 0.0f }, + { Material.DeepslateBricks, 0.0f }, + { Material.DeepslateTileSlab, 0.0f }, + { Material.DeepslateTileWall, 0.0f }, + { Material.DeepslateTiles, 0.0f }, + { Material.DioriteSlab, 0.0f }, + { Material.DioriteWall, 0.0f }, + { Material.DriedGhast, 0.0f }, + { Material.EndRod, 0.0f }, + { Material.EndStoneBrickSlab, 0.0f }, + { Material.EndStoneBrickWall, 0.0f }, + { Material.ExposedChiseledCopper, 0.0f }, + { Material.ExposedCopper, 0.0f }, + { Material.ExposedCopperBulb, 0.0f }, + { Material.ExposedCopperChest, 0.0f }, + { Material.ExposedCopperDoor, 0.0f }, + { Material.ExposedCopperGolemStatue, 0.0f }, + { Material.ExposedCopperGrate, 0.0f }, + { Material.ExposedCopperTrapdoor, 0.0f }, + { Material.ExposedCutCopper, 0.0f }, + { Material.ExposedCutCopperSlab, 0.0f }, + { Material.ExposedCutCopperStairs, 0.0f }, + { Material.ExposedLightningRod, 0.0f }, + { Material.Fern, 0.0f }, + { Material.Fire, 0.0f }, + { Material.FireCoral, 0.0f }, + { Material.FireCoralFan, 0.0f }, + { Material.FireCoralWallFan, 0.0f }, + { Material.FireflyBush, 0.0f }, + { Material.FlowerPot, 0.0f }, + { Material.FloweringAzalea, 0.0f }, + { Material.FloweringAzaleaLeaves, 0.0f }, + { Material.Frogspawn, 0.0f }, + { Material.GildedBlackstone, 0.0f }, + { Material.GlassPane, 0.0f }, + { Material.GraniteSlab, 0.0f }, + { Material.GraniteWall, 0.0f }, + { Material.GrayCandle, 0.0f }, + { Material.GrayCandleCake, 0.0f }, + { Material.GrayShulkerBox, 0.0f }, + { Material.GreenCandle, 0.0f }, + { Material.GreenCandleCake, 0.0f }, + { Material.GreenShulkerBox, 0.0f }, + { Material.HangingRoots, 0.0f }, + { Material.HoneyBlock, 0.0f }, + { Material.HornCoral, 0.0f }, + { Material.HornCoralFan, 0.0f }, + { Material.HornCoralWallFan, 0.0f }, + { Material.InfestedChiseledStoneBricks, 0.0f }, + { Material.InfestedCobblestone, 0.0f }, + { Material.InfestedCrackedStoneBricks, 0.0f }, + { Material.InfestedDeepslate, 0.0f }, + { Material.InfestedMossyStoneBricks, 0.0f }, + { Material.InfestedStone, 0.0f }, + { Material.InfestedStoneBricks, 0.0f }, + { Material.JungleButton, 0.0f }, + { Material.JungleLeaves, 0.0f }, + { Material.JungleLog, 0.0f }, + { Material.JungleSapling, 0.0f }, + { Material.Kelp, 0.0f }, + { Material.KelpPlant, 0.0f }, + { Material.LargeAmethystBud, 0.0f }, + { Material.LargeFern, 0.0f }, + { Material.LavaCauldron, 0.0f }, + { Material.LeafLitter, 0.0f }, + { Material.LightBlueCandle, 0.0f }, + { Material.LightBlueCandleCake, 0.0f }, + { Material.LightBlueShulkerBox, 0.0f }, + { Material.LightGrayCandle, 0.0f }, + { Material.LightGrayCandleCake, 0.0f }, + { Material.LightGrayShulkerBox, 0.0f }, + { Material.Lilac, 0.0f }, + { Material.LilyOfTheValley, 0.0f }, + { Material.LilyPad, 0.0f }, + { Material.LimeCandle, 0.0f }, + { Material.LimeCandleCake, 0.0f }, + { Material.LimeShulkerBox, 0.0f }, + { Material.MagentaCandle, 0.0f }, + { Material.MagentaCandleCake, 0.0f }, + { Material.MagentaShulkerBox, 0.0f }, + { Material.MangroveButton, 0.0f }, + { Material.MangroveLeaves, 0.0f }, + { Material.MangroveLog, 0.0f }, + { Material.MangrovePropagule, 0.0f }, + { Material.MediumAmethystBud, 0.0f }, + { Material.MossyCobblestoneSlab, 0.0f }, + { Material.MossyCobblestoneWall, 0.0f }, + { Material.MossyStoneBrickSlab, 0.0f }, + { Material.MossyStoneBrickWall, 0.0f }, + { Material.Mud, 0.0f }, + { Material.MudBrickWall, 0.0f }, + { Material.NetherBrickWall, 0.0f }, + { Material.NetherSprouts, 0.0f }, + { Material.NetherWart, 0.0f }, + { Material.OakButton, 0.0f }, + { Material.OakLeaves, 0.2f }, + { Material.OakLog, 2.0f }, + { Material.OakSapling, 0.0f }, + { Material.OpenEyeblossom, 0.0f }, + { Material.OrangeCandle, 0.0f }, + { Material.OrangeCandleCake, 0.0f }, + { Material.OrangeShulkerBox, 0.0f }, + { Material.OrangeTulip, 0.0f }, + { Material.OxeyeDaisy, 0.0f }, + { Material.OxidizedChiseledCopper, 0.0f }, + { Material.OxidizedCopper, 0.0f }, + { Material.OxidizedCopperBulb, 0.0f }, + { Material.OxidizedCopperChest, 0.0f }, + { Material.OxidizedCopperDoor, 0.0f }, + { Material.OxidizedCopperGolemStatue, 0.0f }, + { Material.OxidizedCopperGrate, 0.0f }, + { Material.OxidizedCopperTrapdoor, 0.0f }, + { Material.OxidizedCutCopper, 0.0f }, + { Material.OxidizedCutCopperSlab, 0.0f }, + { Material.OxidizedCutCopperStairs, 0.0f }, + { Material.OxidizedLightningRod, 0.0f }, + { Material.PaleHangingMoss, 0.0f }, + { Material.PaleOakButton, 0.0f }, + { Material.PaleOakLog, 0.0f }, + { Material.PaleOakSapling, 0.0f }, + { Material.Peony, 0.0f }, + { Material.PinkCandle, 0.0f }, + { Material.PinkCandleCake, 0.0f }, + { Material.PinkPetals, 0.0f }, + { Material.PinkShulkerBox, 0.0f }, + { Material.PinkTulip, 0.0f }, + { Material.Piston, 0.0f }, + { Material.PitcherCrop, 0.0f }, + { Material.PitcherPlant, 0.0f }, + { Material.PolishedAndesiteSlab, 0.0f }, + { Material.PolishedBlackstoneBrickWall, 0.0f }, + { Material.PolishedBlackstoneButton, 0.0f }, + { Material.PolishedBlackstoneSlab, 0.0f }, + { Material.PolishedBlackstoneWall, 0.0f }, + { Material.PolishedDeepslate, 0.0f }, + { Material.PolishedDeepslateSlab, 0.0f }, + { Material.PolishedDeepslateWall, 0.0f }, + { Material.PolishedDioriteSlab, 0.0f }, + { Material.PolishedGraniteSlab, 0.0f }, + { Material.PolishedTuff, 0.0f }, + { Material.PolishedTuffSlab, 0.0f }, + { Material.PolishedTuffStairs, 0.0f }, + { Material.PolishedTuffWall, 0.0f }, + { Material.Poppy, 0.0f }, + { Material.Potatoes, 0.0f }, + { Material.PottedAcaciaSapling, 0.0f }, + { Material.PottedAllium, 0.0f }, + { Material.PottedAzaleaBush, 0.0f }, + { Material.PottedAzureBluet, 0.0f }, + { Material.PottedBamboo, 0.0f }, + { Material.PottedBirchSapling, 0.0f }, + { Material.PottedBlueOrchid, 0.0f }, + { Material.PottedBrownMushroom, 0.0f }, + { Material.PottedCactus, 0.0f }, + { Material.PottedCherrySapling, 0.0f }, + { Material.PottedClosedEyeblossom, 0.0f }, + { Material.PottedCornflower, 0.0f }, + { Material.PottedCrimsonFungus, 0.0f }, + { Material.PottedCrimsonRoots, 0.0f }, + { Material.PottedDandelion, 0.0f }, + { Material.PottedDarkOakSapling, 0.0f }, + { Material.PottedDeadBush, 0.0f }, + { Material.PottedFern, 0.0f }, + { Material.PottedFloweringAzaleaBush, 0.0f }, + { Material.PottedJungleSapling, 0.0f }, + { Material.PottedLilyOfTheValley, 0.0f }, + { Material.PottedMangrovePropagule, 0.0f }, + { Material.PottedOakSapling, 0.0f }, + { Material.PottedOpenEyeblossom, 0.0f }, + { Material.PottedOrangeTulip, 0.0f }, + { Material.PottedOxeyeDaisy, 0.0f }, + { Material.PottedPaleOakSapling, 0.0f }, + { Material.PottedPinkTulip, 0.0f }, + { Material.PottedPoppy, 0.0f }, + { Material.PottedRedMushroom, 0.0f }, + { Material.PottedRedTulip, 0.0f }, + { Material.PottedSpruceSapling, 0.0f }, + { Material.PottedTorchflower, 0.0f }, + { Material.PottedWarpedFungus, 0.0f }, + { Material.PottedWarpedRoots, 0.0f }, + { Material.PottedWhiteTulip, 0.0f }, + { Material.PottedWitherRose, 0.0f }, + { Material.PowderSnowCauldron, 0.0f }, + { Material.PrismarineWall, 0.0f }, + { Material.PurpleCandle, 0.0f }, + { Material.PurpleCandleCake, 0.0f }, + { Material.PurpleShulkerBox, 0.0f }, + { Material.QuartzBricks, 0.0f }, + { Material.RedCandle, 0.0f }, + { Material.RedCandleCake, 0.0f }, + { Material.RedMushroom, 0.0f }, + { Material.RedNetherBrickSlab, 0.0f }, + { Material.RedNetherBrickWall, 0.0f }, + { Material.RedSandstoneWall, 0.0f }, + { Material.RedShulkerBox, 0.0f }, + { Material.RedTulip, 0.0f }, + { Material.RedstoneTorch, 0.0f }, + { Material.RedstoneWallTorch, 0.0f }, + { Material.RedstoneWire, 0.0f }, + { Material.Repeater, 0.0f }, + { Material.ResinBlock, 0.0f }, + { Material.ResinClump, 0.0f }, + { Material.RoseBush, 0.0f }, + { Material.SandstoneWall, 0.0f }, + { Material.Scaffolding, 0.0f }, + { Material.SeaPickle, 0.0f }, + { Material.Seagrass, 0.0f }, + { Material.ShortDryGrass, 0.0f }, + { Material.ShortGrass, 0.0f }, + { Material.ShulkerBox, 0.0f }, + { Material.SlimeBlock, 0.0f }, + { Material.SmallAmethystBud, 0.0f }, + { Material.SmallDripleaf, 0.0f }, + { Material.SmoothBasalt, 0.0f }, + { Material.SmoothQuartzSlab, 0.0f }, + { Material.SmoothRedSandstoneSlab, 0.0f }, + { Material.SmoothSandstoneSlab, 0.0f }, + { Material.SoulFire, 0.0f }, + { Material.SoulTorch, 0.0f }, + { Material.SoulWallTorch, 0.0f }, + { Material.SporeBlossom, 0.0f }, + { Material.SpruceButton, 0.0f }, + { Material.SpruceLeaves, 0.0f }, + { Material.SpruceLog, 0.0f }, + { Material.SpruceSapling, 0.0f }, + { Material.StickyPiston, 0.0f }, + { Material.StoneBrickWall, 0.0f }, + { Material.StoneButton, 0.0f }, + { Material.StrippedAcaciaLog, 0.0f }, + { Material.StrippedBambooBlock, 0.0f }, + { Material.StrippedBirchLog, 0.0f }, + { Material.StrippedCherryLog, 0.0f }, + { Material.StrippedCrimsonStem, 0.0f }, + { Material.StrippedDarkOakLog, 0.0f }, + { Material.StrippedJungleLog, 0.0f }, + { Material.StrippedMangroveLog, 0.0f }, + { Material.StrippedMangroveWood, 0.0f }, + { Material.StrippedOakLog, 0.0f }, + { Material.StrippedPaleOakLog, 0.0f }, + { Material.StrippedSpruceLog, 0.0f }, + { Material.StrippedWarpedStem, 0.0f }, + { Material.StructureVoid, 0.0f }, + { Material.SugarCane, 0.0f }, + { Material.Sunflower, 0.0f }, + { Material.SweetBerryBush, 0.0f }, + { Material.TallDryGrass, 0.0f }, + { Material.TallGrass, 0.0f }, + { Material.TallSeagrass, 0.0f }, + { Material.TintedGlass, 0.0f }, + { Material.Tnt, 0.0f }, + { Material.Torch, 0.0f }, + { Material.Torchflower, 0.0f }, + { Material.TorchflowerCrop, 0.0f }, + { Material.Tripwire, 0.0f }, + { Material.TripwireHook, 0.0f }, + { Material.TubeCoral, 0.0f }, + { Material.TubeCoralFan, 0.0f }, + { Material.TubeCoralWallFan, 0.0f }, + { Material.TuffBrickSlab, 0.0f }, + { Material.TuffBrickStairs, 0.0f }, + { Material.TuffBrickWall, 0.0f }, + { Material.TuffBricks, 0.0f }, + { Material.TuffSlab, 0.0f }, + { Material.TuffStairs, 0.0f }, + { Material.TuffWall, 0.0f }, + { Material.TwistingVines, 0.0f }, + { Material.TwistingVinesPlant, 0.0f }, + { Material.VoidAir, 0.0f }, + { Material.WallTorch, 0.0f }, + { Material.WarpedButton, 0.0f }, + { Material.WarpedFungus, 0.0f }, + { Material.WarpedRoots, 0.0f }, + { Material.WarpedStem, 0.0f }, + { Material.WaterCauldron, 0.0f }, + { Material.WaxedChiseledCopper, 0.0f }, + { Material.WaxedCopperBlock, 0.0f }, + { Material.WaxedCopperBulb, 0.0f }, + { Material.WaxedCopperChest, 0.0f }, + { Material.WaxedCopperDoor, 0.0f }, + { Material.WaxedCopperGolemStatue, 0.0f }, + { Material.WaxedCopperGrate, 0.0f }, + { Material.WaxedCopperTrapdoor, 0.0f }, + { Material.WaxedCutCopper, 0.0f }, + { Material.WaxedCutCopperSlab, 0.0f }, + { Material.WaxedExposedChiseledCopper, 0.0f }, + { Material.WaxedExposedCopper, 0.0f }, + { Material.WaxedExposedCopperBulb, 0.0f }, + { Material.WaxedExposedCopperChest, 0.0f }, + { Material.WaxedExposedCopperDoor, 0.0f }, + { Material.WaxedExposedCopperGolemStatue, 0.0f }, + { Material.WaxedExposedCopperGrate, 0.0f }, + { Material.WaxedExposedCopperTrapdoor, 0.0f }, + { Material.WaxedExposedCutCopper, 0.0f }, + { Material.WaxedExposedCutCopperSlab, 0.0f }, + { Material.WaxedExposedLightningRod, 0.0f }, + { Material.WaxedLightningRod, 0.0f }, + { Material.WaxedOxidizedChiseledCopper, 0.0f }, + { Material.WaxedOxidizedCopper, 0.0f }, + { Material.WaxedOxidizedCopperBulb, 0.0f }, + { Material.WaxedOxidizedCopperChest, 0.0f }, + { Material.WaxedOxidizedCopperDoor, 0.0f }, + { Material.WaxedOxidizedCopperGolemStatue, 0.0f }, + { Material.WaxedOxidizedCopperGrate, 0.0f }, + { Material.WaxedOxidizedCopperTrapdoor, 0.0f }, + { Material.WaxedOxidizedCutCopper, 0.0f }, + { Material.WaxedOxidizedCutCopperSlab, 0.0f }, + { Material.WaxedOxidizedLightningRod, 0.0f }, + { Material.WaxedWeatheredChiseledCopper, 0.0f }, + { Material.WaxedWeatheredCopper, 0.0f }, + { Material.WaxedWeatheredCopperBulb, 0.0f }, + { Material.WaxedWeatheredCopperChest, 0.0f }, + { Material.WaxedWeatheredCopperDoor, 0.0f }, + { Material.WaxedWeatheredCopperGolemStatue, 0.0f }, + { Material.WaxedWeatheredCopperGrate, 0.0f }, + { Material.WaxedWeatheredCopperTrapdoor, 0.0f }, + { Material.WaxedWeatheredCutCopper, 0.0f }, + { Material.WaxedWeatheredCutCopperSlab, 0.0f }, + { Material.WaxedWeatheredLightningRod, 0.0f }, + { Material.WeatheredChiseledCopper, 0.0f }, + { Material.WeatheredCopper, 0.0f }, + { Material.WeatheredCopperBulb, 0.0f }, + { Material.WeatheredCopperChest, 0.0f }, + { Material.WeatheredCopperDoor, 0.0f }, + { Material.WeatheredCopperGolemStatue, 0.0f }, + { Material.WeatheredCopperGrate, 0.0f }, + { Material.WeatheredCopperTrapdoor, 0.0f }, + { Material.WeatheredCutCopper, 0.0f }, + { Material.WeatheredCutCopperSlab, 0.0f }, + { Material.WeatheredCutCopperStairs, 0.0f }, + { Material.WeatheredLightningRod, 0.0f }, + { Material.WeepingVines, 0.0f }, + { Material.WeepingVinesPlant, 0.0f }, + { Material.Wheat, 0.0f }, + { Material.WhiteCandle, 0.0f }, + { Material.WhiteCandleCake, 0.0f }, + { Material.WhiteShulkerBox, 0.0f }, + { Material.WhiteTulip, 0.0f }, + { Material.Wildflowers, 0.0f }, + { Material.WitherRose, 0.0f }, + { Material.YellowCandle, 0.0f }, + { Material.YellowCandleCake, 0.0f }, + { Material.YellowShulkerBox, 0.0f }, + // Hardness 0.1: 23 blocks + { Material.BigDripleaf, 0.1f }, + { Material.BigDripleafStem, 0.1f }, + { Material.BlackCarpet, 0.1f }, + { Material.BlueCarpet, 0.1f }, + { Material.BrownCarpet, 0.1f }, + { Material.CyanCarpet, 0.1f }, + { Material.GrayCarpet, 0.1f }, + { Material.GreenCarpet, 0.1f }, + { Material.LightBlueCarpet, 0.1f }, + { Material.LightGrayCarpet, 0.1f }, + { Material.LimeCarpet, 0.1f }, + { Material.MagentaCarpet, 0.1f }, + { Material.MossBlock, 0.1f }, + { Material.MossCarpet, 0.1f }, + { Material.OrangeCarpet, 0.1f }, + { Material.PaleMossBlock, 0.1f }, + { Material.PaleMossCarpet, 0.1f }, + { Material.PinkCarpet, 0.1f }, + { Material.PurpleCarpet, 0.1f }, + { Material.RedCarpet, 0.1f }, + { Material.Snow, 0.1f }, + { Material.WhiteCarpet, 0.1f }, + { Material.YellowCarpet, 0.1f }, + // Hardness 0.2: 12 blocks + { Material.BrownMushroomBlock, 0.2f }, + { Material.CherryLeaves, 0.2f }, + { Material.Cocoa, 0.2f }, + { Material.DaylightDetector, 0.2f }, + { Material.GlowLichen, 0.2f }, + { Material.MushroomStem, 0.2f }, + { Material.PaleOakLeaves, 0.2f }, + { Material.RedMushroomBlock, 0.2f }, + { Material.Sculk, 0.2f }, + { Material.SculkVein, 0.2f }, + { Material.SnowBlock, 0.2f }, + { Material.Vine, 0.2f }, + { Material.PowderSnow, 0.25f }, + { Material.SuspiciousGravel, 0.25f }, + { Material.SuspiciousSand, 0.25f }, + // Hardness 0.3: 24 blocks + { Material.BeeNest, 0.3f }, + { Material.BlackStainedGlassPane, 0.3f }, + { Material.BlueStainedGlassPane, 0.3f }, + { Material.BrownStainedGlassPane, 0.3f }, + { Material.CyanStainedGlassPane, 0.3f }, + { Material.Glass, 0.3f }, + { Material.Glowstone, 0.3f }, + { Material.GrayStainedGlassPane, 0.3f }, + { Material.GreenStainedGlassPane, 0.3f }, + { Material.LightBlueStainedGlassPane, 0.3f }, + { Material.LightGrayStainedGlassPane, 0.3f }, + { Material.LimeStainedGlassPane, 0.3f }, + { Material.MagentaStainedGlassPane, 0.3f }, + { Material.OchreFroglight, 0.3f }, + { Material.OrangeStainedGlassPane, 0.3f }, + { Material.PearlescentFroglight, 0.3f }, + { Material.PinkStainedGlassPane, 0.3f }, + { Material.PurpleStainedGlassPane, 0.3f }, + { Material.RedStainedGlassPane, 0.3f }, + { Material.RedstoneLamp, 0.3f }, + { Material.SeaLantern, 0.3f }, + { Material.VerdantFroglight, 0.3f }, + { Material.WhiteStainedGlassPane, 0.3f }, + { Material.YellowStainedGlassPane, 0.3f }, + { Material.Cactus, 0.4f }, + { Material.ChorusFlower, 0.4f }, + { Material.ChorusPlant, 0.4f }, + { Material.CrimsonNylium, 0.4f }, + { Material.Ladder, 0.4f }, + { Material.Netherrack, 0.4f }, + { Material.WarpedNylium, 0.4f }, + // Hardness 0.5: 52 blocks + { Material.AcaciaPressurePlate, 0.5f }, + { Material.BambooPressurePlate, 0.5f }, + { Material.BirchPressurePlate, 0.5f }, + { Material.BlackConcretePowder, 0.5f }, + { Material.BlueConcretePowder, 0.5f }, + { Material.BrewingStand, 0.5f }, + { Material.BrownConcretePowder, 0.5f }, + { Material.Cake, 0.5f }, + { Material.CherryPressurePlate, 0.5f }, + { Material.CoarseDirt, 0.5f }, + { Material.CrimsonPressurePlate, 0.5f }, + { Material.CyanConcretePowder, 0.5f }, + { Material.DarkOakPressurePlate, 0.5f }, + { Material.Dirt, 0.5f }, + { Material.DriedKelpBlock, 0.5f }, + { Material.FrostedIce, 0.5f }, + { Material.GrayConcretePowder, 0.5f }, + { Material.GreenConcretePowder, 0.5f }, + { Material.HayBlock, 0.5f }, + { Material.HeavyWeightedPressurePlate, 0.5f }, + { Material.Ice, 0.5f }, + { Material.JunglePressurePlate, 0.5f }, + { Material.Lever, 0.5f }, + { Material.LightBlueConcretePowder, 0.5f }, + { Material.LightGrayConcretePowder, 0.5f }, + { Material.LightWeightedPressurePlate, 0.5f }, + { Material.LimeConcretePowder, 0.5f }, + { Material.MagentaConcretePowder, 0.5f }, + { Material.MagmaBlock, 0.5f }, + { Material.MangrovePressurePlate, 0.5f }, + { Material.OakPressurePlate, 0.5f }, + { Material.OrangeConcretePowder, 0.5f }, + { Material.PackedIce, 0.5f }, + { Material.PaleOakPressurePlate, 0.5f }, + { Material.PinkConcretePowder, 0.5f }, + { Material.Podzol, 0.5f }, + { Material.PolishedBlackstonePressurePlate, 0.5f }, + { Material.PurpleConcretePowder, 0.5f }, + { Material.RedConcretePowder, 0.5f }, + { Material.RedSand, 0.5f }, + { Material.RootedDirt, 0.5f }, + { Material.Sand, 0.5f }, + { Material.SnifferEgg, 0.5f }, + { Material.SoulSand, 0.5f }, + { Material.SoulSoil, 0.5f }, + { Material.SprucePressurePlate, 0.5f }, + { Material.StonePressurePlate, 0.5f }, + { Material.Target, 0.5f }, + { Material.TurtleEgg, 0.5f }, + { Material.WarpedPressurePlate, 0.5f }, + { Material.WhiteConcretePowder, 0.5f }, + { Material.YellowConcretePowder, 0.5f }, + // Hardness 0.6: 10 blocks + { Material.Beehive, 0.6f }, + { Material.Clay, 0.6f }, + { Material.Composter, 0.6f }, + { Material.Farmland, 0.6f }, + { Material.GrassBlock, 0.6f }, + { Material.Gravel, 0.6f }, + { Material.HoneycombBlock, 0.6f }, + { Material.Mycelium, 0.6f }, + { Material.Sponge, 0.6f }, + { Material.WetSponge, 0.6f }, + { Material.DirtPath, 0.65f }, + { Material.ActivatorRail, 0.7f }, + { Material.DetectorRail, 0.7f }, + { Material.MangroveRoots, 0.7f }, + { Material.MuddyMangroveRoots, 0.7f }, + { Material.PoweredRail, 0.7f }, + { Material.Rail, 0.7f }, + { Material.Calcite, 0.75f }, + // Hardness 0.8: 26 blocks + { Material.BlackWool, 0.8f }, + { Material.BlueWool, 0.8f }, + { Material.BrownWool, 0.8f }, + { Material.ChiseledQuartzBlock, 0.8f }, + { Material.ChiseledRedSandstone, 0.8f }, + { Material.ChiseledSandstone, 0.8f }, + { Material.CutRedSandstone, 0.8f }, + { Material.CutSandstone, 0.8f }, + { Material.CyanWool, 0.8f }, + { Material.GrayWool, 0.8f }, + { Material.GreenWool, 0.8f }, + { Material.LightBlueWool, 0.8f }, + { Material.LightGrayWool, 0.8f }, + { Material.LimeWool, 0.8f }, + { Material.MagentaWool, 0.8f }, + { Material.NoteBlock, 0.8f }, + { Material.OrangeWool, 0.8f }, + { Material.PinkWool, 0.8f }, + { Material.PurpleWool, 0.8f }, + { Material.QuartzBlock, 0.8f }, + { Material.QuartzPillar, 0.8f }, + { Material.RedSandstone, 0.8f }, + { Material.RedWool, 0.8f }, + { Material.Sandstone, 0.8f }, + { Material.WhiteWool, 0.8f }, + { Material.YellowWool, 0.8f }, + // Hardness 1.0: 100 blocks + { Material.AcaciaHangingSign, 1.0f }, + { Material.AcaciaSign, 1.0f }, + { Material.AcaciaWallHangingSign, 1.0f }, + { Material.AcaciaWallSign, 1.0f }, + { Material.BambooHangingSign, 1.0f }, + { Material.BambooSign, 1.0f }, + { Material.BambooWallHangingSign, 1.0f }, + { Material.BambooWallSign, 1.0f }, + { Material.BirchHangingSign, 1.0f }, + { Material.BirchSign, 1.0f }, + { Material.BirchWallHangingSign, 1.0f }, + { Material.BirchWallSign, 1.0f }, + { Material.BlackBanner, 1.0f }, + { Material.BlackWallBanner, 1.0f }, + { Material.BlueBanner, 1.0f }, + { Material.BlueWallBanner, 1.0f }, + { Material.BrownBanner, 1.0f }, + { Material.BrownWallBanner, 1.0f }, + { Material.CarvedPumpkin, 1.0f }, + { Material.CherryHangingSign, 1.0f }, + { Material.CherrySign, 1.0f }, + { Material.CherryWallHangingSign, 1.0f }, + { Material.CherryWallSign, 1.0f }, + { Material.CreeperHead, 1.0f }, + { Material.CreeperWallHead, 1.0f }, + { Material.CrimsonHangingSign, 1.0f }, + { Material.CrimsonSign, 1.0f }, + { Material.CrimsonWallHangingSign, 1.0f }, + { Material.CrimsonWallSign, 1.0f }, + { Material.CyanBanner, 1.0f }, + { Material.CyanWallBanner, 1.0f }, + { Material.DarkOakHangingSign, 1.0f }, + { Material.DarkOakSign, 1.0f }, + { Material.DarkOakWallHangingSign, 1.0f }, + { Material.DarkOakWallSign, 1.0f }, + { Material.DragonHead, 1.0f }, + { Material.DragonWallHead, 1.0f }, + { Material.GrayBanner, 1.0f }, + { Material.GrayWallBanner, 1.0f }, + { Material.GreenBanner, 1.0f }, + { Material.GreenWallBanner, 1.0f }, + { Material.JackOLantern, 1.0f }, + { Material.JungleHangingSign, 1.0f }, + { Material.JungleSign, 1.0f }, + { Material.JungleWallHangingSign, 1.0f }, + { Material.JungleWallSign, 1.0f }, + { Material.LightBlueBanner, 1.0f }, + { Material.LightBlueWallBanner, 1.0f }, + { Material.LightGrayBanner, 1.0f }, + { Material.LightGrayWallBanner, 1.0f }, + { Material.LimeBanner, 1.0f }, + { Material.LimeWallBanner, 1.0f }, + { Material.MagentaBanner, 1.0f }, + { Material.MagentaWallBanner, 1.0f }, + { Material.MangroveHangingSign, 1.0f }, + { Material.MangroveSign, 1.0f }, + { Material.MangroveWallHangingSign, 1.0f }, + { Material.MangroveWallSign, 1.0f }, + { Material.NetherWartBlock, 1.0f }, + { Material.OakHangingSign, 1.0f }, + { Material.OakSign, 1.0f }, + { Material.OakWallHangingSign, 1.0f }, + { Material.OakWallSign, 1.0f }, + { Material.OrangeBanner, 1.0f }, + { Material.OrangeWallBanner, 1.0f }, + { Material.PackedMud, 1.0f }, + { Material.PaleOakHangingSign, 1.0f }, + { Material.PaleOakSign, 1.0f }, + { Material.PaleOakWallHangingSign, 1.0f }, + { Material.PaleOakWallSign, 1.0f }, + { Material.PiglinHead, 1.0f }, + { Material.PiglinWallHead, 1.0f }, + { Material.PinkBanner, 1.0f }, + { Material.PinkWallBanner, 1.0f }, + { Material.PlayerHead, 1.0f }, + { Material.PlayerWallHead, 1.0f }, + { Material.PurpleBanner, 1.0f }, + { Material.PurpleWallBanner, 1.0f }, + { Material.RedBanner, 1.0f }, + { Material.RedWallBanner, 1.0f }, + { Material.Shroomlight, 1.0f }, + { Material.SkeletonSkull, 1.0f }, + { Material.SkeletonWallSkull, 1.0f }, + { Material.SpruceHangingSign, 1.0f }, + { Material.SpruceSign, 1.0f }, + { Material.SpruceWallHangingSign, 1.0f }, + { Material.SpruceWallSign, 1.0f }, + { Material.WarpedHangingSign, 1.0f }, + { Material.WarpedSign, 1.0f }, + { Material.WarpedWallHangingSign, 1.0f }, + { Material.WarpedWallSign, 1.0f }, + { Material.WarpedWartBlock, 1.0f }, + { Material.WhiteBanner, 1.0f }, + { Material.WhiteWallBanner, 1.0f }, + { Material.WitherSkeletonSkull, 1.0f }, + { Material.WitherSkeletonWallSkull, 1.0f }, + { Material.YellowBanner, 1.0f }, + { Material.YellowWallBanner, 1.0f }, + { Material.ZombieHead, 1.0f }, + { Material.ZombieWallHead, 1.0f }, + // Hardness 1.25: 19 blocks + { Material.Basalt, 1.25f }, + { Material.BlackTerracotta, 1.25f }, + { Material.BlueTerracotta, 1.25f }, + { Material.BrownTerracotta, 1.25f }, + { Material.CyanTerracotta, 1.25f }, + { Material.GrayTerracotta, 1.25f }, + { Material.GreenTerracotta, 1.25f }, + { Material.LightBlueTerracotta, 1.25f }, + { Material.LightGrayTerracotta, 1.25f }, + { Material.LimeTerracotta, 1.25f }, + { Material.MagentaTerracotta, 1.25f }, + { Material.OrangeTerracotta, 1.25f }, + { Material.PinkTerracotta, 1.25f }, + { Material.PolishedBasalt, 1.25f }, + { Material.PurpleTerracotta, 1.25f }, + { Material.RedTerracotta, 1.25f }, + { Material.Terracotta, 1.25f }, + { Material.WhiteTerracotta, 1.25f }, + { Material.YellowTerracotta, 1.25f }, + // Hardness 1.4: 16 blocks + { Material.BlackGlazedTerracotta, 1.4f }, + { Material.BlueGlazedTerracotta, 1.4f }, + { Material.BrownGlazedTerracotta, 1.4f }, + { Material.CyanGlazedTerracotta, 1.4f }, + { Material.GrayGlazedTerracotta, 1.4f }, + { Material.GreenGlazedTerracotta, 1.4f }, + { Material.LightBlueGlazedTerracotta, 1.4f }, + { Material.LightGrayGlazedTerracotta, 1.4f }, + { Material.LimeGlazedTerracotta, 1.4f }, + { Material.MagentaGlazedTerracotta, 1.4f }, + { Material.OrangeGlazedTerracotta, 1.4f }, + { Material.PinkGlazedTerracotta, 1.4f }, + { Material.PurpleGlazedTerracotta, 1.4f }, + { Material.RedGlazedTerracotta, 1.4f }, + { Material.WhiteGlazedTerracotta, 1.4f }, + { Material.YellowGlazedTerracotta, 1.4f }, + // Hardness 1.5: 49 blocks + { Material.AmethystBlock, 1.5f }, + { Material.AmethystCluster, 1.5f }, + { Material.Andesite, 1.5f }, + { Material.Blackstone, 1.5f }, + { Material.Bookshelf, 1.5f }, + { Material.BrainCoralBlock, 1.5f }, + { Material.BubbleCoralBlock, 1.5f }, + { Material.BuddingAmethyst, 1.5f }, + { Material.ChiseledBookshelf, 1.5f }, + { Material.ChiseledPolishedBlackstone, 1.5f }, + { Material.ChiseledResinBricks, 1.5f }, + { Material.ChiseledStoneBricks, 1.5f }, + { Material.CrackedStoneBricks, 1.5f }, + { Material.Crafter, 1.5f }, + { Material.DarkPrismarine, 1.5f }, + { Material.DarkPrismarineSlab, 1.5f }, + { Material.DeadBrainCoralBlock, 1.5f }, + { Material.DeadBubbleCoralBlock, 1.5f }, + { Material.DeadFireCoralBlock, 1.5f }, + { Material.DeadHornCoralBlock, 1.5f }, + { Material.DeadTubeCoralBlock, 1.5f }, + { Material.Diorite, 1.5f }, + { Material.DripstoneBlock, 1.5f }, + { Material.FireCoralBlock, 1.5f }, + { Material.Granite, 1.5f }, + { Material.HornCoralBlock, 1.5f }, + { Material.MossyStoneBricks, 1.5f }, + { Material.MudBrickSlab, 1.5f }, + { Material.MudBricks, 1.5f }, + { Material.PistonHead, 1.5f }, + { Material.PointedDripstone, 1.5f }, + { Material.PolishedAndesite, 1.5f }, + { Material.PolishedBlackstoneBricks, 1.5f }, + { Material.PolishedDiorite, 1.5f }, + { Material.PolishedGranite, 1.5f }, + { Material.Prismarine, 1.5f }, + { Material.PrismarineBrickSlab, 1.5f }, + { Material.PrismarineBricks, 1.5f }, + { Material.PrismarineSlab, 1.5f }, + { Material.PurpurBlock, 1.5f }, + { Material.PurpurPillar, 1.5f }, + { Material.ResinBrickSlab, 1.5f }, + { Material.ResinBrickWall, 1.5f }, + { Material.ResinBricks, 1.5f }, + { Material.SculkSensor, 1.5f }, + { Material.Stone, 1.5f }, + { Material.StoneBricks, 1.5f }, + { Material.TubeCoralBlock, 1.5f }, + { Material.Tuff, 1.5f }, + // Hardness 1.8: 16 blocks + { Material.BlackConcrete, 1.8f }, + { Material.BlueConcrete, 1.8f }, + { Material.BrownConcrete, 1.8f }, + { Material.CyanConcrete, 1.8f }, + { Material.GrayConcrete, 1.8f }, + { Material.GreenConcrete, 1.8f }, + { Material.LightBlueConcrete, 1.8f }, + { Material.LightGrayConcrete, 1.8f }, + { Material.LimeConcrete, 1.8f }, + { Material.MagentaConcrete, 1.8f }, + { Material.OrangeConcrete, 1.8f }, + { Material.PinkConcrete, 1.8f }, + { Material.PurpleConcrete, 1.8f }, + { Material.RedConcrete, 1.8f }, + { Material.WhiteConcrete, 1.8f }, + { Material.YellowConcrete, 1.8f }, + // Hardness 2.0: 117 blocks + { Material.AcaciaFence, 2.0f }, + { Material.AcaciaFenceGate, 2.0f }, + { Material.AcaciaPlanks, 2.0f }, + { Material.AcaciaShelf, 2.0f }, + { Material.AcaciaSlab, 2.0f }, + { Material.AcaciaWood, 2.0f }, + { Material.BambooFence, 2.0f }, + { Material.BambooFenceGate, 2.0f }, + { Material.BambooMosaic, 2.0f }, + { Material.BambooMosaicSlab, 2.0f }, + { Material.BambooPlanks, 2.0f }, + { Material.BambooShelf, 2.0f }, + { Material.BambooSlab, 2.0f }, + { Material.BirchFence, 2.0f }, + { Material.BirchFenceGate, 2.0f }, + { Material.BirchPlanks, 2.0f }, + { Material.BirchShelf, 2.0f }, + { Material.BirchSlab, 2.0f }, + { Material.BirchWood, 2.0f }, + { Material.BlackstoneSlab, 2.0f }, + { Material.BoneBlock, 2.0f }, + { Material.BrickSlab, 2.0f }, + { Material.Bricks, 2.0f }, + { Material.Campfire, 2.0f }, + { Material.Cauldron, 2.0f }, + { Material.CherryFence, 2.0f }, + { Material.CherryFenceGate, 2.0f }, + { Material.CherryPlanks, 2.0f }, + { Material.CherryShelf, 2.0f }, + { Material.CherrySlab, 2.0f }, + { Material.CherryWood, 2.0f }, + { Material.ChiseledNetherBricks, 2.0f }, + { Material.Cobblestone, 2.0f }, + { Material.CobblestoneSlab, 2.0f }, + { Material.CrackedNetherBricks, 2.0f }, + { Material.CrimsonFence, 2.0f }, + { Material.CrimsonFenceGate, 2.0f }, + { Material.CrimsonHyphae, 2.0f }, + { Material.CrimsonPlanks, 2.0f }, + { Material.CrimsonShelf, 2.0f }, + { Material.CrimsonSlab, 2.0f }, + { Material.CutRedSandstoneSlab, 2.0f }, + { Material.CutSandstoneSlab, 2.0f }, + { Material.DarkOakFence, 2.0f }, + { Material.DarkOakFenceGate, 2.0f }, + { Material.DarkOakPlanks, 2.0f }, + { Material.DarkOakShelf, 2.0f }, + { Material.DarkOakSlab, 2.0f }, + { Material.DarkOakWood, 2.0f }, + { Material.Grindstone, 2.0f }, + { Material.Jukebox, 2.0f }, + { Material.JungleFence, 2.0f }, + { Material.JungleFenceGate, 2.0f }, + { Material.JunglePlanks, 2.0f }, + { Material.JungleShelf, 2.0f }, + { Material.JungleSlab, 2.0f }, + { Material.JungleWood, 2.0f }, + { Material.MangroveFence, 2.0f }, + { Material.MangroveFenceGate, 2.0f }, + { Material.MangrovePlanks, 2.0f }, + { Material.MangroveShelf, 2.0f }, + { Material.MangroveSlab, 2.0f }, + { Material.MangroveWood, 2.0f }, + { Material.MossyCobblestone, 2.0f }, + { Material.NetherBrickFence, 2.0f }, + { Material.NetherBrickSlab, 2.0f }, + { Material.NetherBricks, 2.0f }, + { Material.OakFence, 2.0f }, + { Material.OakFenceGate, 2.0f }, + { Material.OakPlanks, 2.0f }, + { Material.OakShelf, 2.0f }, + { Material.OakSlab, 2.0f }, + { Material.OakWood, 2.0f }, + { Material.PaleOakFence, 2.0f }, + { Material.PaleOakFenceGate, 2.0f }, + { Material.PaleOakPlanks, 2.0f }, + { Material.PaleOakShelf, 2.0f }, + { Material.PaleOakSlab, 2.0f }, + { Material.PaleOakWood, 2.0f }, + { Material.PetrifiedOakSlab, 2.0f }, + { Material.PolishedBlackstone, 2.0f }, + { Material.PolishedBlackstoneBrickSlab, 2.0f }, + { Material.PurpurSlab, 2.0f }, + { Material.QuartzSlab, 2.0f }, + { Material.RedNetherBricks, 2.0f }, + { Material.RedSandstoneSlab, 2.0f }, + { Material.SandstoneSlab, 2.0f }, + { Material.SmoothQuartz, 2.0f }, + { Material.SmoothRedSandstone, 2.0f }, + { Material.SmoothSandstone, 2.0f }, + { Material.SmoothStone, 2.0f }, + { Material.SmoothStoneSlab, 2.0f }, + { Material.SoulCampfire, 2.0f }, + { Material.SpruceFence, 2.0f }, + { Material.SpruceFenceGate, 2.0f }, + { Material.SprucePlanks, 2.0f }, + { Material.SpruceShelf, 2.0f }, + { Material.SpruceSlab, 2.0f }, + { Material.SpruceWood, 2.0f }, + { Material.StoneBrickSlab, 2.0f }, + { Material.StoneSlab, 2.0f }, + { Material.StrippedAcaciaWood, 2.0f }, + { Material.StrippedBirchWood, 2.0f }, + { Material.StrippedCherryWood, 2.0f }, + { Material.StrippedCrimsonHyphae, 2.0f }, + { Material.StrippedDarkOakWood, 2.0f }, + { Material.StrippedJungleWood, 2.0f }, + { Material.StrippedOakWood, 2.0f }, + { Material.StrippedPaleOakWood, 2.0f }, + { Material.StrippedSpruceWood, 2.0f }, + { Material.StrippedWarpedHyphae, 2.0f }, + { Material.WarpedFence, 2.0f }, + { Material.WarpedFenceGate, 2.0f }, + { Material.WarpedHyphae, 2.0f }, + { Material.WarpedPlanks, 2.0f }, + { Material.WarpedShelf, 2.0f }, + { Material.WarpedSlab, 2.0f }, + // Hardness 2.5: 9 blocks + { Material.Barrel, 2.5f }, + { Material.CartographyTable, 2.5f }, + { Material.Chest, 2.5f }, + { Material.CraftingTable, 2.5f }, + { Material.FletchingTable, 2.5f }, + { Material.Lectern, 2.5f }, + { Material.Loom, 2.5f }, + { Material.SmithingTable, 2.5f }, + { Material.TrappedChest, 2.5f }, + { Material.BlueIce, 2.8f }, + // Hardness 3.0: 53 blocks + { Material.AcaciaDoor, 3.0f }, + { Material.AcaciaTrapdoor, 3.0f }, + { Material.BambooDoor, 3.0f }, + { Material.BambooTrapdoor, 3.0f }, + { Material.Beacon, 3.0f }, + { Material.BirchDoor, 3.0f }, + { Material.BirchTrapdoor, 3.0f }, + { Material.CherryDoor, 3.0f }, + { Material.CherryTrapdoor, 3.0f }, + { Material.CoalOre, 3.0f }, + { Material.Conduit, 3.0f }, + { Material.CopperBlock, 3.0f }, + { Material.CopperBulb, 3.0f }, + { Material.CopperChest, 3.0f }, + { Material.CopperDoor, 3.0f }, + { Material.CopperGolemStatue, 3.0f }, + { Material.CopperGrate, 3.0f }, + { Material.CopperTrapdoor, 3.0f }, + { Material.CrimsonDoor, 3.0f }, + { Material.CrimsonTrapdoor, 3.0f }, + { Material.DarkOakDoor, 3.0f }, + { Material.DarkOakTrapdoor, 3.0f }, + { Material.Deepslate, 3.0f }, + { Material.DiamondOre, 3.0f }, + { Material.DragonEgg, 3.0f }, + { Material.EmeraldOre, 3.0f }, + { Material.EndStone, 3.0f }, + { Material.EndStoneBricks, 3.0f }, + { Material.GoldBlock, 3.0f }, + { Material.GoldOre, 3.0f }, + { Material.Hopper, 3.0f }, + { Material.IronOre, 3.0f }, + { Material.JungleDoor, 3.0f }, + { Material.JungleTrapdoor, 3.0f }, + { Material.LapisBlock, 3.0f }, + { Material.LapisOre, 3.0f }, + { Material.LightningRod, 3.0f }, + { Material.MangroveDoor, 3.0f }, + { Material.MangroveTrapdoor, 3.0f }, + { Material.NetherGoldOre, 3.0f }, + { Material.NetherQuartzOre, 3.0f }, + { Material.OakDoor, 3.0f }, + { Material.OakTrapdoor, 3.0f }, + { Material.Observer, 3.0f }, + { Material.PaleOakDoor, 3.0f }, + { Material.PaleOakTrapdoor, 3.0f }, + { Material.RedstoneOre, 3.0f }, + { Material.SculkCatalyst, 3.0f }, + { Material.SculkShrieker, 3.0f }, + { Material.SpruceDoor, 3.0f }, + { Material.SpruceTrapdoor, 3.0f }, + { Material.WarpedDoor, 3.0f }, + { Material.WarpedTrapdoor, 3.0f }, + // Hardness 3.5: 10 blocks + { Material.BlastFurnace, 3.5f }, + { Material.CobbledDeepslate, 3.5f }, + { Material.Dispenser, 3.5f }, + { Material.Dropper, 3.5f }, + { Material.Furnace, 3.5f }, + { Material.Lantern, 3.5f }, + { Material.Lodestone, 3.5f }, + { Material.Smoker, 3.5f }, + { Material.SoulLantern, 3.5f }, + { Material.Stonecutter, 3.5f }, + { Material.Cobweb, 4.0f }, + { Material.DeepslateCoalOre, 4.5f }, + { Material.DeepslateCopperOre, 4.5f }, + { Material.DeepslateDiamondOre, 4.5f }, + { Material.DeepslateEmeraldOre, 4.5f }, + { Material.DeepslateGoldOre, 4.5f }, + { Material.DeepslateIronOre, 4.5f }, + { Material.DeepslateLapisOre, 4.5f }, + { Material.DeepslateRedstoneOre, 4.5f }, + // Hardness 5.0: 18 blocks + { Material.Anvil, 5.0f }, + { Material.Bell, 5.0f }, + { Material.ChippedAnvil, 5.0f }, + { Material.CoalBlock, 5.0f }, + { Material.DamagedAnvil, 5.0f }, + { Material.DiamondBlock, 5.0f }, + { Material.EmeraldBlock, 5.0f }, + { Material.EnchantingTable, 5.0f }, + { Material.IronBars, 5.0f }, + { Material.IronBlock, 5.0f }, + { Material.IronChain, 5.0f }, + { Material.IronDoor, 5.0f }, + { Material.IronTrapdoor, 5.0f }, + { Material.RawCopperBlock, 5.0f }, + { Material.RawGoldBlock, 5.0f }, + { Material.RawIronBlock, 5.0f }, + { Material.RedstoneBlock, 5.0f }, + { Material.Spawner, 5.0f }, + { Material.CreakingHeart, 10.0f }, + { Material.HeavyCore, 10.0f }, + { Material.EnderChest, 22.5f }, + { Material.AncientDebris, 30.0f }, + { Material.CryingObsidian, 50.0f }, + { Material.NetheriteBlock, 50.0f }, + { Material.Obsidian, 50.0f }, + { Material.RespawnAnchor, 50.0f }, + { Material.TrialSpawner, 50.0f }, + { Material.Vault, 50.0f }, + { Material.ReinforcedDeepslate, 55.0f }, + { Material.Lava, 100.0f }, + { Material.Water, 100.0f }, + }.ToFrozenDictionary(); + + private static readonly FrozenSet RequiresCorrectToolSet = new HashSet + { + Material.AmethystBlock, + Material.AncientDebris, + Material.Andesite, + Material.Anvil, + Material.Basalt, + Material.BlackConcrete, + Material.BlackGlazedTerracotta, + Material.BlackTerracotta, + Material.Blackstone, + Material.BlastFurnace, + Material.BlueConcrete, + Material.BlueGlazedTerracotta, + Material.BlueTerracotta, + Material.BoneBlock, + Material.BrainCoralBlock, + Material.BrickSlab, + Material.Bricks, + Material.BrownConcrete, + Material.BrownGlazedTerracotta, + Material.BrownTerracotta, + Material.BubbleCoralBlock, + Material.BuddingAmethyst, + Material.Calcite, + Material.Cauldron, + Material.ChainCommandBlock, + Material.ChippedAnvil, + Material.ChiseledNetherBricks, + Material.ChiseledQuartzBlock, + Material.ChiseledRedSandstone, + Material.ChiseledResinBricks, + Material.ChiseledSandstone, + Material.ChiseledStoneBricks, + Material.CoalBlock, + Material.CoalOre, + Material.Cobblestone, + Material.CobblestoneSlab, + Material.Cobweb, + Material.CommandBlock, + Material.CopperBlock, + Material.CopperBulb, + Material.CopperChest, + Material.CopperGrate, + Material.CopperTrapdoor, + Material.CrackedNetherBricks, + Material.CrackedStoneBricks, + Material.CrimsonNylium, + Material.CryingObsidian, + Material.CutRedSandstone, + Material.CutRedSandstoneSlab, + Material.CutSandstone, + Material.CutSandstoneSlab, + Material.CyanConcrete, + Material.CyanGlazedTerracotta, + Material.CyanTerracotta, + Material.DamagedAnvil, + Material.DarkPrismarine, + Material.DarkPrismarineSlab, + Material.DeadBrainCoral, + Material.DeadBrainCoralBlock, + Material.DeadBrainCoralFan, + Material.DeadBrainCoralWallFan, + Material.DeadBubbleCoral, + Material.DeadBubbleCoralBlock, + Material.DeadBubbleCoralFan, + Material.DeadBubbleCoralWallFan, + Material.DeadFireCoral, + Material.DeadFireCoralBlock, + Material.DeadFireCoralFan, + Material.DeadFireCoralWallFan, + Material.DeadHornCoral, + Material.DeadHornCoralBlock, + Material.DeadHornCoralFan, + Material.DeadHornCoralWallFan, + Material.DeadTubeCoral, + Material.DeadTubeCoralBlock, + Material.DeadTubeCoralFan, + Material.DeadTubeCoralWallFan, + Material.Deepslate, + Material.DiamondBlock, + Material.DiamondOre, + Material.Diorite, + Material.Dispenser, + Material.DripstoneBlock, + Material.Dropper, + Material.EmeraldBlock, + Material.EmeraldOre, + Material.EnchantingTable, + Material.EndStone, + Material.EndStoneBricks, + Material.FireCoralBlock, + Material.Furnace, + Material.GoldBlock, + Material.GoldOre, + Material.Granite, + Material.GrayConcrete, + Material.GrayGlazedTerracotta, + Material.GrayTerracotta, + Material.GreenConcrete, + Material.GreenGlazedTerracotta, + Material.GreenTerracotta, + Material.Grindstone, + Material.Hopper, + Material.HornCoralBlock, + Material.IronBars, + Material.IronBlock, + Material.IronChain, + Material.IronOre, + Material.IronTrapdoor, + Material.Jigsaw, + Material.LapisBlock, + Material.LapisOre, + Material.LightBlueConcrete, + Material.LightBlueGlazedTerracotta, + Material.LightBlueTerracotta, + Material.LightGrayConcrete, + Material.LightGrayGlazedTerracotta, + Material.LightGrayTerracotta, + Material.LightningRod, + Material.LimeConcrete, + Material.LimeGlazedTerracotta, + Material.LimeTerracotta, + Material.Lodestone, + Material.MagentaConcrete, + Material.MagentaGlazedTerracotta, + Material.MagentaTerracotta, + Material.MagmaBlock, + Material.MossyCobblestone, + Material.MossyStoneBricks, + Material.MudBrickSlab, + Material.MudBricks, + Material.NetherBrickFence, + Material.NetherBrickSlab, + Material.NetherBricks, + Material.NetherGoldOre, + Material.NetherQuartzOre, + Material.NetheriteBlock, + Material.Netherrack, + Material.Observer, + Material.Obsidian, + Material.OrangeConcrete, + Material.OrangeGlazedTerracotta, + Material.OrangeTerracotta, + Material.PetrifiedOakSlab, + Material.PinkConcrete, + Material.PinkGlazedTerracotta, + Material.PinkTerracotta, + Material.PolishedAndesite, + Material.PolishedBasalt, + Material.PolishedDiorite, + Material.PolishedGranite, + Material.Prismarine, + Material.PrismarineBrickSlab, + Material.PrismarineBricks, + Material.PrismarineSlab, + Material.PurpleConcrete, + Material.PurpleGlazedTerracotta, + Material.PurpleTerracotta, + Material.PurpurBlock, + Material.PurpurPillar, + Material.PurpurSlab, + Material.QuartzBlock, + Material.QuartzPillar, + Material.QuartzSlab, + Material.RawCopperBlock, + Material.RawGoldBlock, + Material.RawIronBlock, + Material.RedConcrete, + Material.RedGlazedTerracotta, + Material.RedNetherBricks, + Material.RedSandstone, + Material.RedSandstoneSlab, + Material.RedTerracotta, + Material.RedstoneBlock, + Material.RedstoneOre, + Material.RepeatingCommandBlock, + Material.ResinBrickSlab, + Material.ResinBrickWall, + Material.ResinBricks, + Material.RespawnAnchor, + Material.Sandstone, + Material.SandstoneSlab, + Material.Smoker, + Material.SmoothQuartz, + Material.SmoothRedSandstone, + Material.SmoothSandstone, + Material.SmoothStone, + Material.SmoothStoneSlab, + Material.Snow, + Material.SnowBlock, + Material.Spawner, + Material.Stone, + Material.StoneBrickSlab, + Material.StoneBricks, + Material.StoneSlab, + Material.Stonecutter, + Material.StructureBlock, + Material.Terracotta, + Material.TubeCoralBlock, + Material.Tuff, + Material.WarpedNylium, + Material.WaxedCutCopperSlab, + Material.WaxedExposedCutCopperSlab, + Material.WaxedOxidizedCutCopperSlab, + Material.WaxedWeatheredCutCopperSlab, + Material.WhiteConcrete, + Material.WhiteGlazedTerracotta, + Material.WhiteTerracotta, + Material.YellowConcrete, + Material.YellowGlazedTerracotta, + Material.YellowTerracotta, + }.ToFrozenSet(); + } +} diff --git a/MinecraftClient/Mapping/MiningCalculator.cs b/MinecraftClient/Mapping/MiningCalculator.cs new file mode 100644 index 00000000..6f49fd4a --- /dev/null +++ b/MinecraftClient/Mapping/MiningCalculator.cs @@ -0,0 +1,572 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using MinecraftClient.Inventory; +using MinecraftClient.Protocol.Handlers; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_5; +using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6; + +namespace MinecraftClient.Mapping +{ + /// + /// Computes dig duration in ticks for survival-style block breaking. + /// Version-aware across 1.8-1.21.11+, using tool speed, enchantments, effects, and attributes. + /// + public static class MiningCalculator + { + /// + /// Compute the number of ticks required to break a block in survival mode. + /// Returns 0 for instant-break blocks, -1 for unbreakable blocks. + /// + /// The block material to break + /// The item in the player's main hand (null for empty hand) + /// The item in the player's helmet slot (null if empty, used for Aqua Affinity) + /// Currently active player effects + /// Cached player attribute values (from OnEntityProperties) + /// Whether the player's eyes are submerged in water + /// Whether the player is on the ground + /// The Minecraft protocol version + /// Ticks to break the block, 0 for instant, -1 for unbreakable + public static int ComputeDigTicks( + Material blockMaterial, + Item? heldItem, + Item? helmetItem, + Dictionary effects, + Dictionary playerAttributes, + bool isUnderwater, + bool isOnGround, + int protocolVersion) + { + float hardness = BlockHardness.GetHardness(blockMaterial); + + if (hardness < 0) + return -1; // Unbreakable + + if (hardness == 0) + return 0; // Instant break + + float destroySpeed = GetDestroySpeed( + blockMaterial, heldItem, helmetItem, effects, playerAttributes, + isUnderwater, isOnGround, protocolVersion); + + bool correctTool = HasCorrectToolForDrops(blockMaterial, heldItem, protocolVersion); + int divisor = correctTool ? 30 : 100; + + float destroyProgress = destroySpeed / hardness / divisor; + + if (destroyProgress >= 1.0f) + return 0; // Instant break + + return (int)MathF.Ceiling(1.0f / destroyProgress); + } + + /// + /// Compute the player's destroy speed for a given block, following vanilla formulas. + /// + private static float GetDestroySpeed( + Material blockMaterial, + Item? heldItem, + Item? helmetItem, + Dictionary effects, + Dictionary playerAttributes, + bool isUnderwater, + bool isOnGround, + int protocolVersion) + { + float speed = GetToolSpeed(blockMaterial, heldItem, protocolVersion); + + if (speed > 1.0f) + { + speed += GetEfficiencyBonus(heldItem, playerAttributes, protocolVersion); + } + + int digSpeedAmplifier = GetDigSpeedAmplifier(effects); + if (digSpeedAmplifier >= 0) + speed *= 1.0f + (digSpeedAmplifier + 1) * 0.2f; + + // Mining Fatigue + if (effects.TryGetValue(Effects.MiningFatigue, out var fatigueData)) + { + float multiplier = fatigueData.Amplifier switch + { + 0 => 0.3f, + 1 => 0.09f, + 2 => 0.0027f, + _ => 8.1E-4f + }; + speed *= multiplier; + } + + // Attribute multipliers for modern versions + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version) + { + // BLOCK_BREAK_SPEED attribute (default 1.0) + if (playerAttributes.TryGetValue("player.block_break_speed", out double bbs)) + speed *= (float)bbs; + } + + // Underwater penalty + if (isUnderwater) + { + if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version) + { + // 1.21.11+: Uses SUBMERGED_MINING_SPEED attribute (default 0.2) + double submergedSpeed = 0.2; + if (playerAttributes.TryGetValue("player.submerged_mining_speed", out double sms)) + submergedSpeed = sms; + speed *= (float)submergedSpeed; + } + else + { + // Pre-1.21.11: /5 unless Aqua Affinity + bool hasAquaAffinity = GetEnchantmentLevel(helmetItem, Enchantments.AquaAffinity, protocolVersion) > 0; + if (!hasAquaAffinity) + speed /= 5.0f; + } + } + + // Airborne penalty + if (!isOnGround) + speed /= 5.0f; + + return speed; + } + + /// + /// Get the base tool mining speed for a block. + /// For 1.20.6+ with ToolComponent, uses structured component data. + /// For older versions, uses hardcoded tool speed tables. + /// + private static float GetToolSpeed(Material blockMaterial, Item? heldItem, int protocolVersion) + { + if (heldItem is null) + return 1.0f; + + // Modern path: use ToolComponent from structured components + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version + && TryGetToolRules(heldItem, out List? rules, out float defaultMiningSpeed)) + { + foreach (var rule in rules) + { + if (rule.HasSpeed && MatchesBlockSet(rule.Blocks, blockMaterial)) + return rule.Speed; + } + + // Structured tool data covers modern mining rules, but keep the legacy fallback for + // explicit block holder-sets that MCC cannot resolve yet (for example cobweb). + if (defaultMiningSpeed > 1.0f) + return defaultMiningSpeed; + } + + // Legacy path: hardcoded tool speed tables + return GetLegacyToolSpeed(heldItem.Type, blockMaterial); + } + + /// + /// Check whether the tool provides correct drops for a block. + /// + private static bool HasCorrectToolForDrops(Material blockMaterial, Item? heldItem, int protocolVersion) + { + if (!BlockHardness.RequiresCorrectTool(blockMaterial)) + return true; + + if (heldItem is null) + return false; + + // Modern path: check ToolComponent rules + if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version + && TryGetToolRules(heldItem, out List? rules, out _)) + { + foreach (var rule in rules) + { + if (rule.HasCorrectDropForBlocks && MatchesBlockSet(rule.Blocks, blockMaterial)) + return rule.CorrectDropForBlocks; + } + } + + // Legacy path, plus a modern fallback for direct block holder-sets MCC cannot resolve yet. + return IsCorrectToolLegacy(heldItem.Type, blockMaterial); + } + + private static bool TryGetToolRules( + Item heldItem, + [NotNullWhen(true)] out List? rules, + out float defaultMiningSpeed) + { + rules = null; + defaultMiningSpeed = 1.0f; + + if (heldItem.Components is null) + return false; + + if (heldItem.Components.OfType().FirstOrDefault() is ToolComponent toolComponent) + { + rules = toolComponent.Rules; + defaultMiningSpeed = toolComponent.DefaultMiningSpeed; + return true; + } + + if (heldItem.Components.OfType().FirstOrDefault() is ToolComponent1215 toolComponent1215) + { + rules = toolComponent1215.Rules; + defaultMiningSpeed = toolComponent1215.DefaultMiningSpeed; + return true; + } + + return false; + } + + /// + /// Match a block material against a ToolComponent BlockSetSubcomponent. + /// + private static bool MatchesBlockSet( + Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6.BlockSetSubcomponent blockSet, + Material blockMaterial) + { + if (blockSet.BlockIds is not null) + { + // Check against explicit block state IDs + foreach (int blockId in blockSet.BlockIds) + { + if (Block.Palette.FromId(blockId) == blockMaterial) + return true; + } + } + + if (blockSet.TagName is not null) + { + // Match against tag name (e.g., "minecraft:mineable/pickaxe") + return MatchesBlockTag(blockSet.TagName, blockMaterial); + } + + return false; + } + + /// + /// Approximate block tag matching using Material2Tool categories. + /// Tags like "minecraft:mineable/pickaxe" map to the appropriate tool categories. + /// + private static bool MatchesBlockTag(string tagName, Material blockMaterial) + { + // Normalize tag name + string tag = tagName.Replace("minecraft:", ""); + + ItemType[] tools = Material2Tool.GetCorrectToolForBlock(blockMaterial); + return tag switch + { + "mineable/pickaxe" => tools.Length > 0 && IsPickaxe(tools[0]), + "mineable/axe" => tools.Length > 0 && IsAxe(tools[0]), + "mineable/shovel" => tools.Length > 0 && IsShovel(tools[0]), + "mineable/hoe" => tools.Length > 0 && IsHoe(tools[0]), + "leaves" => IsLeaf(blockMaterial), + "wool" => IsWool(blockMaterial), + "incorrect_for_wooden_tool" => RequiresHigherTier(blockMaterial, 0), + "incorrect_for_gold_tool" => RequiresHigherTier(blockMaterial, 0), + "incorrect_for_stone_tool" => RequiresHigherTier(blockMaterial, 1), + "incorrect_for_copper_tool" => RequiresHigherTier(blockMaterial, 1), + "incorrect_for_iron_tool" => RequiresHigherTier(blockMaterial, 2), + "incorrect_for_diamond_tool" => RequiresHigherTier(blockMaterial, 3), + "incorrect_for_netherite_tool" => RequiresHigherTier(blockMaterial, 4), + _ => false + }; + } + + private static bool RequiresHigherTier(Material blockMaterial, int tier) + { + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (recommended.Length == 0) + return false; + + return GetRequiredTier(blockMaterial, recommended) > tier; + } + + /// + /// Get the enchantment level from an item, supporting both legacy NBT and modern structured components. + /// + public static int GetEnchantmentLevel(Item? item, Enchantments enchantment, int protocolVersion) + { + if (item is null) + return 0; + + // Modern path: structured components (1.20.6+) + var enchList = item.EnchantmentList; + if (enchList is not null) + { + var ench = enchList.FirstOrDefault(e => e.Type == enchantment); + if (ench is not null) + return ench.Level; + } + + // Legacy path: NBT data + if (item.NBT is not null && + item.NBT.TryGetValue("Enchantments", out object? enchantments)) + { + try + { + string enchNameLower = GetEnchantmentResourceName(enchantment); + foreach (Dictionary enchEntry in (object[])enchantments) + { + string id = ((string)enchEntry["id"]).ToLowerInvariant(); + if (id == enchNameLower || id == "minecraft:" + enchNameLower) + return (short)enchEntry["lvl"]; + } + } + catch + { + // NBT parsing failure - return 0 + } + } + + return 0; + } + + /// + /// Map Enchantments enum to Minecraft resource name (e.g., "efficiency"). + /// + private static string GetEnchantmentResourceName(Enchantments enchantment) + { + return enchantment switch + { + Enchantments.AquaAffinity => "aqua_affinity", + Enchantments.BaneOfArthropods => "bane_of_arthropods", + Enchantments.BlastProtection => "blast_protection", + Enchantments.Efficiency => "efficiency", + Enchantments.FeatherFalling => "feather_falling", + Enchantments.FireAspect => "fire_aspect", + Enchantments.FireProtection => "fire_protection", + Enchantments.FrostWalker => "frost_walker", + Enchantments.LuckOfTheSea => "luck_of_the_sea", + Enchantments.ProjectileProtection => "projectile_protection", + Enchantments.QuickCharge => "quick_charge", + Enchantments.SilkTouch => "silk_touch", + Enchantments.SoulSpeed => "soul_speed", + Enchantments.SwiftSneak => "swift_sneak", + Enchantments.VanishingCurse => "vanishing_curse", + Enchantments.BindingCurse => "binding_curse", + Enchantments.WindBurst => "wind_burst", + _ => enchantment.ToString().ToUnderscoreCase() + }; + } + + #region Legacy Tool Speed Tables + + /// + /// Legacy tool speed for pre-1.20.6 versions using hardcoded values. + /// + private static float GetLegacyToolSpeed(ItemType toolType, Material blockMaterial) + { + float specialToolSpeed = toolType switch + { + ItemType.Shears => GetShearsSpeed(blockMaterial), + _ when IsSword(toolType) && blockMaterial == Material.Cobweb => 15.0f, + _ => 1.0f + }; + + if (specialToolSpeed > 1.0f) + return specialToolSpeed; + + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (recommended.Length == 0) + return 1.0f; + + // Check if the held tool matches the recommended tool category + ToolCategory heldCategory = GetToolCategory(toolType); + ToolCategory neededCategory = GetToolCategory(recommended[0]); + + if (heldCategory == ToolCategory.None || heldCategory != neededCategory) + return 1.0f; + + return GetBaseToolSpeed(toolType); + } + + private static float GetBaseToolSpeed(ItemType toolType) + { + return toolType switch + { + // Wooden tools + ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or + ItemType.WoodenSword or ItemType.WoodenHoe => 2.0f, + + // Stone tools + ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or + ItemType.StoneSword or ItemType.StoneHoe => 4.0f, + + // Iron tools + ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or + ItemType.IronSword or ItemType.IronHoe => 6.0f, + + // Diamond tools + ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or + ItemType.DiamondSword or ItemType.DiamondHoe => 8.0f, + + // Netherite tools + ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or + ItemType.NetheriteSword or ItemType.NetheriteHoe => 9.0f, + + // Golden tools + ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or + ItemType.GoldenSword or ItemType.GoldenHoe => 12.0f, + + // Shears + ItemType.Shears => 2.0f, + + _ => 1.0f + }; + } + + /// + /// Check if the held tool is the correct tool for drops in legacy versions. + /// Uses Material2Tool's recommendations to determine correctness. + /// + private static bool IsCorrectToolLegacy(ItemType toolType, Material blockMaterial) + { + ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial); + if (recommended.Length == 0) + return false; + + ToolCategory heldCategory = GetToolCategory(toolType); + ToolCategory neededCategory = GetToolCategory(recommended[0]); + + if (heldCategory == ToolCategory.None || heldCategory != neededCategory) + { + if (toolType == ItemType.Shears && blockMaterial == Material.Cobweb) + return true; + if (IsSword(toolType) && blockMaterial == Material.Cobweb) + return true; + return false; + } + + // Check tool tier requirement + int heldTier = GetToolTier(toolType); + int requiredTier = GetRequiredTier(blockMaterial, recommended); + + return heldTier >= requiredTier; + } + + /// + /// Get the minimum tool tier required for a block based on Material2Tool's recommendation ordering. + /// + private static int GetRequiredTier(Material blockMaterial, ItemType[] recommended) + { + if (recommended.Length == 0) + return 0; + + // Material2Tool lists tools from highest to lowest tier. + // The last tool in the array is the minimum required tier. + return GetToolTier(recommended[^1]); + } + + private enum ToolCategory + { + None, + Pickaxe, + Axe, + Shovel, + Hoe, + Sword, + Shears + } + + private static ToolCategory GetToolCategory(ItemType item) + { + if (IsPickaxe(item)) return ToolCategory.Pickaxe; + if (IsAxe(item)) return ToolCategory.Axe; + if (IsShovel(item)) return ToolCategory.Shovel; + if (IsHoe(item)) return ToolCategory.Hoe; + if (IsSword(item)) return ToolCategory.Sword; + if (item == ItemType.Shears) return ToolCategory.Shears; + return ToolCategory.None; + } + + private static int GetToolTier(ItemType item) + { + string name = item.ToString(); + if (name.StartsWith("Wooden")) return 0; + if (name.StartsWith("Golden")) return 0; + if (name.StartsWith("Stone")) return 1; + if (name.StartsWith("Iron")) return 2; + if (name.StartsWith("Diamond")) return 3; + if (name.StartsWith("Netherite")) return 4; + return 0; + } + + private static bool IsPickaxe(ItemType item) => + item is ItemType.WoodenPickaxe or ItemType.StonePickaxe or ItemType.IronPickaxe + or ItemType.GoldenPickaxe or ItemType.DiamondPickaxe or ItemType.NetheritePickaxe; + + private static bool IsAxe(ItemType item) => + item is ItemType.WoodenAxe or ItemType.StoneAxe or ItemType.IronAxe + or ItemType.GoldenAxe or ItemType.DiamondAxe or ItemType.NetheriteAxe; + + private static bool IsShovel(ItemType item) => + item is ItemType.WoodenShovel or ItemType.StoneShovel or ItemType.IronShovel + or ItemType.GoldenShovel or ItemType.DiamondShovel or ItemType.NetheriteShovel; + + private static bool IsHoe(ItemType item) => + item is ItemType.WoodenHoe or ItemType.StoneHoe or ItemType.IronHoe + or ItemType.GoldenHoe or ItemType.DiamondHoe or ItemType.NetheriteHoe; + + private static bool IsSword(ItemType item) => + item is ItemType.WoodenSword or ItemType.StoneSword or ItemType.IronSword + or ItemType.GoldenSword or ItemType.DiamondSword or ItemType.NetheriteSword; + + private static float GetShearsSpeed(Material block) + { + return block switch + { + Material.Cobweb => 15.0f, + Material.Vine or Material.GlowLichen => 2.0f, + _ when IsLeaf(block) => 15.0f, + _ when IsWool(block) => 5.0f, + _ => 1.0f + }; + } + + private static bool IsShearable(Material block) => + block == Material.Cobweb || IsLeaf(block) || IsWool(block) || block is Material.Vine or Material.GlowLichen; + + private static bool IsLeaf(Material block) => + block is Material.OakLeaves or Material.SpruceLeaves or Material.BirchLeaves + or Material.JungleLeaves or Material.AcaciaLeaves or Material.DarkOakLeaves + or Material.CherryLeaves or Material.MangroveLeaves or Material.AzaleaLeaves + or Material.FloweringAzaleaLeaves or Material.PaleOakLeaves; + + private static bool IsWool(Material block) => + block is Material.WhiteWool or Material.OrangeWool or Material.MagentaWool + or Material.LightBlueWool or Material.YellowWool or Material.LimeWool + or Material.PinkWool or Material.GrayWool or Material.LightGrayWool + or Material.CyanWool or Material.PurpleWool or Material.BlueWool + or Material.BrownWool or Material.GreenWool or Material.RedWool + or Material.BlackWool; + + private static float GetEfficiencyBonus(Item? heldItem, Dictionary playerAttributes, int protocolVersion) + { + if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version + && playerAttributes.TryGetValue("player.mining_efficiency", out double miningEfficiency) + && miningEfficiency > 0.0) + { + return (float)miningEfficiency; + } + + int efficiencyLevel = GetEnchantmentLevel(heldItem, Enchantments.Efficiency, protocolVersion); + return efficiencyLevel > 0 ? efficiencyLevel * efficiencyLevel + 1 : 0.0f; + } + + private static int GetDigSpeedAmplifier(Dictionary effects) + { + int amplifier = -1; + + if (effects.TryGetValue(Effects.Haste, out var hasteData)) + amplifier = Math.Max(amplifier, hasteData.Amplifier); + + if (effects.TryGetValue(Effects.ConduitPower, out var conduitData)) + amplifier = Math.Max(amplifier, conduitData.Amplifier); + + return amplifier; + } + + #endregion + } +} diff --git a/MinecraftClient/Mapping/Movement.cs b/MinecraftClient/Mapping/Movement.cs index 6786dbee..0e972e09 100644 --- a/MinecraftClient/Mapping/Movement.cs +++ b/MinecraftClient/Mapping/Movement.cs @@ -232,8 +232,8 @@ namespace MinecraftClient.Mapping int tentativeGScore = current.GScore + (int)current.Location.DistanceSquared(neighbor); // If the neighbor is not in the GScoreDict OR its current tentativeGScore is lower than the previously saved one: - if (!gScoreDict.ContainsKey(neighbor) || - (gScoreDict.ContainsKey(neighbor) && tentativeGScore < gScoreDict[neighbor])) + if (!gScoreDict.TryGetValue(neighbor, out int existingGScore) || + tentativeGScore < existingGScore) { // Save the new relation between the neighbored block and the current one cameFrom[neighbor] = current.Location; diff --git a/MinecraftClient/Mapping/PlayerTeam.cs b/MinecraftClient/Mapping/PlayerTeam.cs new file mode 100644 index 00000000..4ad7ca64 --- /dev/null +++ b/MinecraftClient/Mapping/PlayerTeam.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; + +namespace MinecraftClient.Mapping +{ + /// + /// Represents a Minecraft scoreboard team and its current state. + /// + public class PlayerTeam + { + /// Team internal name (up to 16 chars) + public string Name { get; set; } = string.Empty; + + /// Display name component (formatted text) + public string DisplayName { get; set; } = string.Empty; + + /// Friendly fire is allowed between team members + public bool AllowFriendlyFire { get; set; } + + /// Team members can see invisible teammates + public bool SeeFriendlyInvisibles { get; set; } + + /// + /// Nametag visibility rule. + /// Values: "always", "hideForOtherTeams", "hideForOwnTeam", "never" + /// + public string NameTagVisibility { get; set; } = string.Empty; + + /// + /// Collision rule. + /// Values: "always", "pushOtherTeams", "pushOwnTeam", "never" + /// + public string CollisionRule { get; set; } = string.Empty; + + /// + /// Team color as ChatFormatting enum ordinal (-1 = RESET/none, + /// 0–15 = BLACK … WHITE). + /// + public int Color { get; set; } = -1; + + /// Prefix displayed before member names (formatted text) + public string Prefix { get; set; } = string.Empty; + + /// Suffix displayed after member names (formatted text) + public string Suffix { get; set; } = string.Empty; + + /// Current set of player / entity names on this team + public HashSet Members { get; } = new(System.StringComparer.OrdinalIgnoreCase); + } +} diff --git a/MinecraftClient/Mapping/World.cs b/MinecraftClient/Mapping/World.cs index 66290a40..6e4c4ecc 100644 --- a/MinecraftClient/Mapping/World.cs +++ b/MinecraftClient/Mapping/World.cs @@ -12,9 +12,9 @@ namespace MinecraftClient.Mapping { /// /// The chunks contained into the Minecraft world - /// Tuple: Tuple + /// (int ChunkX, int ChunkZ): chunkX, chunkZ /// - private ConcurrentDictionary, ChunkColumn> chunks = new(); + private ConcurrentDictionary<(int ChunkX, int ChunkZ), ChunkColumn> chunks = new(); /// /// The dimension info of the world @@ -49,12 +49,12 @@ namespace MinecraftClient.Mapping { get { - chunks.TryGetValue(new(chunkX, chunkZ), out ChunkColumn? chunkColumn); + chunks.TryGetValue((chunkX, chunkZ), out ChunkColumn? chunkColumn); return chunkColumn; } set { - Tuple chunkCoord = new(chunkX, chunkZ); + var chunkCoord = (chunkX, chunkZ); if (value is null) chunks.TryRemove(chunkCoord, out _); else @@ -361,7 +361,7 @@ namespace MinecraftClient.Mapping /// Whether the ChunkColumn has been fully loaded public void StoreChunk(int chunkX, int chunkY, int chunkZ, int chunkColumnSize, Chunk? chunk, bool loadCompleted) { - ChunkColumn chunkColumn = chunks.GetOrAdd(new(chunkX, chunkZ), (_) => new(chunkColumnSize)); + ChunkColumn chunkColumn = chunks.GetOrAdd((chunkX, chunkZ), (_) => new(chunkColumnSize)); chunkColumn[chunkY] = chunk; if (loadCompleted) chunkColumn.FullyLoaded = true; diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index bae74f75..4d23bb60 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -44,10 +44,15 @@ namespace MinecraftClient private readonly Queue threadTasks = new(); private readonly Lock threadTasksLock = new(); + private readonly Lock recipeBookLock = new(); + private readonly Lock achievementsLock = new(); private readonly List bots = new(); private static readonly List botsOnHold = new(); private static readonly Dictionary inventories = new(); + private readonly Dictionary unlockedRecipes = new(StringComparer.Ordinal); + private readonly Dictionary achievements = new(StringComparer.Ordinal); + private string? activeAdvancementTab; private readonly Dictionary> registeredBotPluginChannels = new(); private readonly List registeredServerPluginChannels = new(); @@ -105,6 +110,12 @@ namespace MinecraftClient // player effects private readonly Dictionary playerEffects = new(); + + // player attributes (e.g., block_break_speed, mining_efficiency, submerged_mining_speed) + private readonly Dictionary playerAttributes = new(); + + // scoreboard teams (key = team name) + private readonly Dictionary teams = new(StringComparer.Ordinal); // Sneaking public bool IsSneaking { get; set; } = false; @@ -156,6 +167,30 @@ namespace MinecraftClient return new Dictionary(playerEffects); } + /// + /// Get a snapshot of all known scoreboard teams. + /// + /// Dictionary mapping team name to + public Dictionary GetTeams() + { + lock (teams) + return new Dictionary(teams, StringComparer.Ordinal); + } + + /// + /// Get the team that contains the given player/entity name, or null if not found. + /// + public PlayerTeam? GetPlayerTeam(string playerName) + { + lock (teams) + { + foreach (var team in teams.Values) + if (team.Members.Contains(playerName)) + return team; + return null; + } + } + public int GetLevel() { return playerLevel; } public int GetTotalExperience() { return playerTotalExperience; } public byte GetCurrentSlot() { return CurrentSlot; } @@ -1257,6 +1292,7 @@ namespace MinecraftClient inventoryHandlingEnabled = false; inventoryHandlingRequested = false; inventories.Clear(); + ClearUnlockedRecipes(); } return true; } @@ -1358,6 +1394,54 @@ namespace MinecraftClient return lastEnchantment; } + /// + /// Get all unlocked recipe book recipe identifiers. + /// + /// Unlocked recipe identifiers sorted alphabetically + public RecipeBookRecipeEntry[] GetUnlockedRecipes() + { + lock (recipeBookLock) + { + return unlockedRecipes.Values.OrderBy(static recipe => recipe.CommandId, StringComparer.Ordinal).ToArray(); + } + } + + /// + /// Get all achievements/advancements known to the client. + /// + /// Snapshot of all achievements + public Achievement[] GetAchievements() + { + lock (achievementsLock) + { + return [.. achievements.Values]; + } + } + + /// + /// Get only completed achievements/advancements. + /// + /// Snapshot of completed achievements + public Achievement[] GetUnlockedAchievements() + { + lock (achievementsLock) + { + return achievements.Values.Where(static a => a.IsCompleted).ToArray(); + } + } + + /// + /// Get only incomplete achievements/advancements. + /// + /// Snapshot of locked achievements + public Achievement[] GetLockedAchievements() + { + lock (achievementsLock) + { + return achievements.Values.Where(static a => !a.IsCompleted).ToArray(); + } + } + /// /// Get all Entities /// @@ -1404,6 +1488,22 @@ namespace MinecraftClient return GetInventory(0)!; } + /// + /// Get the currently active inventory if it supports recipe book crafting. + /// + /// Active recipe book inventory, or null if the active inventory does not support recipe book crafting + public Container? GetActiveRecipeBookInventory() + { + if (InvokeRequired) + return InvokeOnMainThread(() => GetActiveRecipeBookInventory()); + + if (inventories.Count == 0) + return null; + + Container activeInventory = inventories.MaxBy(static pair => pair.Key).Value; + return SupportsRecipeBook(activeInventory.Type) ? activeInventory : null; + } + /// /// Get a set of online player names /// @@ -1499,6 +1599,12 @@ namespace MinecraftClient if (String.IsNullOrEmpty(text)) return; + if (!CanSendMessage) + { + Log.Warn(Translations.mcc_send_text_not_connected); + return; + } + int maxLength = handler.GetMaxChatMessageLength(); lock (chatQueue) @@ -2516,6 +2622,7 @@ namespace MinecraftClient inventories.Clear(); inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory"); + ClearUnlockedRecipes(); return true; } @@ -2599,6 +2706,13 @@ namespace MinecraftClient if (lookAtBlock) UpdateLocation(GetCurrentLocation(), location); + // Auto-compute dig duration for survival/adventure mode when not explicitly supplied + if (duration <= 0 && protocolversion >= Protocol18Handler.MC_1_8_Version + && gamemode is 0 or 2) // Survival or Adventure + { + duration = ComputeAutoDigDuration(location); + } + // Send dig start and dig end, will need to wait for server response to know dig result // See https://wiki.vg/How_to_Write_a_Client#Digging for more details bool result = handler.SendPlayerDigging(0, location, blockFace, sequenceId++) @@ -2616,6 +2730,52 @@ namespace MinecraftClient } } + /// + /// Compute the automatic dig duration in seconds for a block, based on held tool, + /// enchantments, effects, attributes, and player state. + /// Returns 0 for instant-break blocks. + /// + private double ComputeAutoDigDuration(Location location) + { + try + { + Block block = world.GetBlock(location); + Material blockMaterial = block.Type; + + if (blockMaterial == Material.Air) + return 0; + + // Get held item from player inventory + Item? heldItem = null; + Item? helmetItem = null; + if (inventories.TryGetValue(0, out var playerInv)) + { + int hotbarSlot = 36 + CurrentSlot; // Hotbar slots are 36-44 + playerInv.Items.TryGetValue(hotbarSlot, out heldItem); + playerInv.Items.TryGetValue(5, out helmetItem); // Slot 5 = helmet + } + + int ticks = MiningCalculator.ComputeDigTicks( + blockMaterial, + heldItem, + helmetItem, + playerEffects, + playerAttributes, + playerPhysics.InWater, + playerPhysics.OnGround, + protocolversion); + + if (ticks <= 0) + return 0; + + return (double)ticks / Settings.ClientTicksPerSecond; + } + catch + { + return 0; + } + } + /// /// Change active slot in the player inventory /// @@ -2752,6 +2912,31 @@ namespace MinecraftClient return handler.SendRenameItem(itemName); } + + /// + /// Send a recipe book craft request for the currently active crafting inventory. + /// + /// Recipe identifier to craft + /// True to craft as many items as possible + /// True if the packet was sent + public bool SendPlaceRecipe(string recipeId, bool makeAll) + { + if (InvokeRequired) + return InvokeOnMainThread(() => SendPlaceRecipe(recipeId, makeAll)); + + if (protocolversion < Protocol18Handler.MC_1_13_Version) + return false; + + Container? activeInventory = GetActiveRecipeBookInventory(); + if (activeInventory is null) + return false; + + string normalizedRecipeId = NormalizeRecipeArgument(recipeId, protocolversion); + if (normalizedRecipeId.Length == 0) + return false; + + return handler.SendPlaceRecipe(activeInventory.ID, normalizedRecipeId, makeAll); + } #endregion #region Event handlers: An event occurs on the Server @@ -3737,6 +3922,44 @@ namespace MinecraftClient } } + /// + /// Called when an entity velocity update is received. + /// + /// Entity ID + /// Velocity on X axis (blocks/tick) + /// Velocity on Y axis (blocks/tick) + /// Velocity on Z axis (blocks/tick) + public void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ) + { + if (entities.TryGetValue(entityID, out Entity? entity)) + DispatchBotEvent(bot => bot.OnEntityVelocity(entity, velocityX, velocityY, velocityZ)); + } + + /// + /// Called when a sound packet is received. + /// + /// Sound key when available, otherwise null + /// Sound location when available + /// Sound category id from packet + /// Sound volume + /// Sound pitch + /// Source entity id for entity sound packets, if any + public void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + int? entityID) + { + Entity? sourceEntity = null; + Location? resolvedLocation = location; + + if (entityID is int id && entities.TryGetValue(id, out Entity? entity)) + { + sourceEntity = entity; + resolvedLocation ??= entity.Location; + } + + DispatchBotEvent(bot => bot.OnSoundEffect(soundName, resolvedLocation, category, volume, pitch, + sourceEntity)); + } + /// /// Called when received entity properties from server. /// @@ -3746,6 +3969,9 @@ namespace MinecraftClient { if (EntityID == playerEntityID) { + foreach (var kvp in prop) + playerAttributes[kvp.Key] = kvp.Value; + DispatchBotEvent(bot => bot.OnPlayerProperty(prop)); } } @@ -3777,11 +4003,15 @@ namespace MinecraftClient { DateTime currentTime = DateTime.Now; long tickDiff = WorldAge - lastAge; - Double tps = tickDiff / (currentTime - lastTime).TotalSeconds; + double tps = tickDiff / (currentTime - lastTime).TotalSeconds; lastAge = WorldAge; lastTime = currentTime; - if (tps <= 20 && tps > 0) + if (tps > 0) { + // A Minecraft server cannot genuinely exceed 20 TPS; values above 20 are + // caused by packet-timing jitter. Clamp instead of discarding so that a + // healthy server averages to 20 rather than being biased downward. + tps = Math.Min(tps, 20.0); // calculate average tps if (tpsSamples.Count >= maxSamples) { @@ -3951,7 +4181,78 @@ namespace MinecraftClient { DispatchBotEvent(bot => bot.OnUpdateScore(entityName, action, objectiveName, objectiveDisplayName, objectiveValue, numberFormat)); } - + + /// + /// Called when a Teams packet is received. Updates the internal team state and notifies bots. + /// + public void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags, + string nameTagVisibility, string collisionRule, int color, + string prefix, string suffix, List players) + { + lock (teams) + { + switch (method) + { + case 0: // create + var newTeam = new PlayerTeam + { + Name = teamName, + DisplayName = displayName, + AllowFriendlyFire = (friendlyFlags & 0x01) != 0, + SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0, + NameTagVisibility = nameTagVisibility, + CollisionRule = collisionRule, + Color = color, + Prefix = prefix, + Suffix = suffix + }; + foreach (var p in players) + newTeam.Members.Add(p); + teams[teamName] = newTeam; + break; + + case 1: // remove + teams.Remove(teamName); + break; + + case 2: // update parameters + if (!teams.TryGetValue(teamName, out var updateTeam)) + { + updateTeam = new PlayerTeam { Name = teamName }; + teams[teamName] = updateTeam; + } + updateTeam.DisplayName = displayName; + updateTeam.AllowFriendlyFire = (friendlyFlags & 0x01) != 0; + updateTeam.SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0; + updateTeam.NameTagVisibility = nameTagVisibility; + updateTeam.CollisionRule = collisionRule; + updateTeam.Color = color; + updateTeam.Prefix = prefix; + updateTeam.Suffix = suffix; + break; + + case 3: // add players + if (!teams.TryGetValue(teamName, out var addTeam)) + { + addTeam = new PlayerTeam { Name = teamName }; + teams[teamName] = addTeam; + } + foreach (var p in players) + addTeam.Members.Add(p); + break; + + case 4: // remove players + if (teams.TryGetValue(teamName, out var removeTeam)) + foreach (var p in players) + removeTeam.Members.Remove(p); + break; + } + } + DispatchBotEvent(bot => bot.OnTeam(teamName, method, displayName, friendlyFlags, + nameTagVisibility, collisionRule, color, prefix, suffix, players)); + } + + /// /// Called when the client received the Tab Header and Footer /// @@ -4152,6 +4453,95 @@ namespace MinecraftClient Log.Debug("CanSendMessage = " + canSendMessage); } + public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace) + { + lock (recipeBookLock) + { + if (replace) + unlockedRecipes.Clear(); + + foreach (RecipeBookRecipeEntry recipe in recipes) + { + // Guard against malformed server packets that send empty display IDs. + if (!string.IsNullOrWhiteSpace(recipe.CommandId)) + unlockedRecipes[recipe.CommandId] = recipe; + } + } + } + + public void OnRecipeBookRemove(string[] recipeIds) + { + lock (recipeBookLock) + { + foreach (string recipeId in recipeIds) + { + if (!string.IsNullOrWhiteSpace(recipeId)) + unlockedRecipes.Remove(recipeId); + } + } + } + + public void OnAchievementsUpdate(IReadOnlyList added, IReadOnlyList removedIds, bool reset) + { + lock (achievementsLock) + { + if (reset) + achievements.Clear(); + + // Remove entries + foreach (string id in removedIds) + achievements.Remove(id); + + // Add/update entries. For progress-only updates (no definition), + // merge with existing definition if available. + foreach (Achievement entry in added) + { + if (entry.Title is null && achievements.TryGetValue(entry.Id, out Achievement? existing)) + { + // Progress-only update - merge with existing definition + bool isCompleted = ComputeAchievementCompleted(existing.Requirements, entry.CriteriaProgress); + achievements[entry.Id] = existing with { IsCompleted = isCompleted, CriteriaProgress = entry.CriteriaProgress }; + } + else + { + achievements[entry.Id] = entry; + } + } + } + + DispatchBotEvent(bot => bot.OnAchievementUpdate(added, removedIds, reset)); + } + + public void OnSelectAdvancementTab(string? tabId) + { + activeAdvancementTab = tabId; + } + + /// + /// Compute whether an achievement is completed based on AND-of-ORs requirements. + /// + private static bool ComputeAchievementCompleted(IReadOnlyList> requirements, IReadOnlyDictionary criteria) + { + if (requirements.Count == 0) + return true; + + foreach (IReadOnlyList group in requirements) + { + bool groupSatisfied = false; + foreach (string criterion in group) + { + if (criteria.TryGetValue(criterion, out bool done) && done) + { + groupSatisfied = true; + break; + } + } + if (!groupSatisfied) + return false; + } + return true; + } + /// /// Send a click container button packet to the server. /// Used for Enchanting table, Lectern, stone cutter and loom @@ -4296,6 +4686,51 @@ namespace MinecraftClient return ((int)blockLocation.X, (int)blockLocation.Y, (int)blockLocation.Z); } + private static bool SupportsRecipeBook(ContainerType containerType) + { + return containerType switch + { + ContainerType.PlayerInventory or + ContainerType.Crafting or + ContainerType.Furnace or + ContainerType.BlastFurnace or + ContainerType.Smoker or + ContainerType.Stonecutter => true, + _ => false, + }; + } + + private void ClearUnlockedRecipes() + { + lock (recipeBookLock) + { + unlockedRecipes.Clear(); + } + } + + /// + /// Normalize a recipe argument for the target protocol version. + /// Legacy recipe-book packets use identifiers and default to the minecraft namespace. + /// 1.21.2+ recipe-book packets use numeric recipe display ids and should be left trimmed-only. + /// + internal static string NormalizeRecipeArgument(string recipeId, int protocolVersion) + { + return protocolVersion >= Protocol18Handler.MC_1_21_2_Version + ? recipeId.Trim() + : NormalizeRecipeId(recipeId); + } + + private static string NormalizeRecipeId(string recipeId) + { + string trimmedRecipeId = recipeId.Trim(); + if (trimmedRecipeId.Length == 0) + return string.Empty; + + return trimmedRecipeId.Contains(':', StringComparison.Ordinal) + ? trimmedRecipeId + : "minecraft:" + trimmedRecipeId; + } + #endregion } } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 4fdf6c94..e8e9206b 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -21,6 +21,8 @@ + + diff --git a/MinecraftClient/Physics/BlockShapes.cs b/MinecraftClient/Physics/BlockShapes.cs index 535c1ee4..c960913c 100644 --- a/MinecraftClient/Physics/BlockShapes.cs +++ b/MinecraftClient/Physics/BlockShapes.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.IO; using System.Linq; @@ -19,7 +20,7 @@ namespace MinecraftClient.Physics private static readonly Aabb[] FullBlockArray = { FullBlock }; private static readonly Aabb[] EmptyArray = Array.Empty(); - private static Dictionary? stateToShape; + private static FrozenDictionary? stateToShape; private static Dictionary? prismarineBlocks; private static Dictionary? prismarineShapes; @@ -136,14 +137,21 @@ namespace MinecraftClient.Physics private static void BuildStateMap() { - stateToShape = new Dictionary(); + var builder = new Dictionary(); if (prismarineBlocks is null || prismarineShapes is null) + { + stateToShape = builder.ToFrozenDictionary(); return; + } var palette = Block.Palette; var dict = GetPaletteDict(palette); - if (dict is null) return; + if (dict is null) + { + stateToShape = builder.ToFrozenDictionary(); + return; + } // Group consecutive state IDs by Material to find state ranges per block var materialRanges = new Dictionary>(); @@ -182,20 +190,20 @@ namespace MinecraftClient.Physics { var shapes = prismarineShapes.GetValueOrDefault(singleShapeId, EmptyArray); for (int sid = start; sid <= end; sid++) - stateToShape[sid] = shapes; + builder[sid] = shapes; } else if (blockShapeData is List shapeIdList) { for (int i = 0; i < stateCount && (globalStateOffset + i) < shapeIdList.Count; i++) { int shapeId = shapeIdList[globalStateOffset + i]; - stateToShape[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray); + builder[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray); } } globalStateOffset += stateCount; } } - + stateToShape = builder.ToFrozenDictionary(); } /// diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 3ce80d79..ad78111d 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -161,14 +163,7 @@ namespace MinecraftClient } if (configResult.NeedWriteDefault) - { Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage(); - WriteBackSettings(false); - } - else if (configResult.Success) - { - WriteBackSettings(true); - } if (!Config.Main.Advanced.EnableSentry) _sentrySdk?.Dispose(); @@ -181,12 +176,22 @@ namespace MinecraftClient }; // --- Determine console mode and initialize backend --- + if (!OperatingSystem.IsWindows()) + InstallCursesNativeResolver(); + if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui) { ConsoleIO.Backend?.Shutdown(); - var tuiBackend = new Tui.TuiConsoleBackend(); - ConsoleIO.Backend = tuiBackend; - tuiBackend.RunTuiMainLoop(args, startupState); + try + { + var tuiBackend = new Tui.TuiConsoleBackend(); + ConsoleIO.Backend = tuiBackend; + tuiBackend.RunTuiMainLoop(args, startupState); + } + catch (Exception ex) + { + HandleTuiStartupFailure(ex); + } return; } @@ -200,9 +205,46 @@ namespace MinecraftClient if (!ProcessStartupState(startupState)) return; + // Wait for this issue to be fixed before enabling it: https://github.com/Consolonia/Consolonia/issues/602 + // MaybePrintClassicModeTuiRecommendation(); + RunStartupSequence(args); } + /// + /// Consolonia's Unix.Terminal uses [DllImport("libcoreclr.so")] to reach + /// dlopen/dlsym on .NET Core. The library ships a + /// SetDllImportResolver that maps libcoreclr.so to the current + /// process, but it is compiled under #if NET6_0 (exact TFM match) instead + /// of NET6_0_OR_GREATER, so it is dead code when the consuming project + /// targets net8.0+. On a self-contained single-file publish the physical + /// libcoreclr.so does not exist on the search path, causing a + /// DllNotFoundException that crashes the TUI. + /// + /// We work around this by registering our own resolver before any Consolonia + /// code runs: if any assembly asks for libcoreclr.so we return + /// (IntPtr)(-1) which the runtime interprets as "the current process". + /// + private static void InstallCursesNativeResolver() + { + AssemblyLoadContext.Default.ResolvingUnmanagedDll += (assembly, libraryName) => + libraryName == "libcoreclr.so" ? (IntPtr)(-1) : IntPtr.Zero; + } + + private static void HandleTuiStartupFailure(Exception exception) + { + Config.Console.General.ConsoleMode = ConsoleModeType.classic; + WriteBackSettings(enableBackup: false); + + ConsoleIO.Backend = new ClassicConsoleBackend(); + ConsoleIO.Backend.Init(); + + ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_tui_startup_failed); + ConsoleIO.WriteLine(exception.ToString()); + ConsoleIO.WriteLineFormatted("§e" + Translations.mcc_report_issue); + ConsoleIO.WriteLineFormatted("§e" + Translations.mcc_tui_startup_fallback_classic); + } + /// /// Prints the application banner and processes the startup state collected before /// the console backend was ready. Called once from classic mode or from TUI after @@ -211,14 +253,33 @@ namespace MinecraftClient /// True if startup can continue; false if config load failed and user chose to exit. internal static bool ProcessStartupState(StartupState state) { - ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam"); - if (BuildInfo is not null) - ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + if (Config.Console.General.Display_Icon_Banner && ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBanner) + { + var view = tuiBanner.GetView(); + if (view is not null) + { + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + var panel = Tui.MccBannerPanelBuilder.Build(BuildInfo); + view.AppendControlToLog(panel); + }); + } + else + { + ShowClassicBanner(); + } + } + else + { + ShowClassicBanner(); + } var cfg = state.ConfigResult; if (cfg.NeedWriteDefault) { + WriteBackSettings(false); + if (cfg.IsLegacyUpgrade) { ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config); @@ -243,6 +304,8 @@ namespace MinecraftClient } else { + WriteBackSettings(true); + if (!Config.Main.Advanced.Language.StartsWith("en")) ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl)); } @@ -250,6 +313,26 @@ namespace MinecraftClient return true; } + private static void ShowClassicBanner() + { + ConsoleIO.WriteLine(string.Format(Translations.mcc_banner_classic, Version, MCLowestVersion, MCHighestVersion, "Github.com/MCCTeam")); + if (BuildInfo is not null) + ConsoleIO.WriteLineFormatted("§8" + BuildInfo); + } + + private static void MaybePrintClassicModeTuiRecommendation() + { + if (ConsoleIO.BasicIO + || Config.Console.General.ConsoleMode != ConsoleModeType.classic + || Console.IsInputRedirected) + { + return; + } + + char cmdChar = Config.Main.Advanced.InternalCmdChar.ToChar(); + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_console_mode_tui_recommendation, cmdChar)); + } + /// /// Handles a failed config load by prompting the user to fix or regenerate the config file. /// diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 78456c2d..bdbdecea 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -411,6 +411,37 @@ namespace MinecraftClient.Protocol.Handlers return ReadNextNbt(cache, true); } + /// + /// Read an ItemStackTemplate (26.1+) from a cache of bytes. + /// Unlike ItemStack, this uses item-first encoding: item_id, count, DataComponentPatch. + /// ItemStackTemplate is always non-empty (no count=0 sentinel). + /// + public Item ReadNextItemStackTemplate(Queue cache, ItemPalette itemPalette) + { + var itemId = ReadNextVarInt(cache); + var itemCount = ReadNextVarInt(cache); + var item = new Item(itemPalette.FromId(itemId), itemCount, null); + + var numberOfComponentsToAdd = ReadNextVarInt(cache); + var numberofComponentsToRemove = ReadNextVarInt(cache); + var structuredComponentHandler = new StructuredComponentsHandler(protocolversion, this, itemPalette); + var strcturedComponentsToAdd = new List(numberOfComponentsToAdd); + + for (var i = 0; i < numberOfComponentsToAdd; i++) + { + var componentTypeId = ReadNextVarInt(cache); + strcturedComponentsToAdd.Add(structuredComponentHandler.Parse(componentTypeId, cache)); + } + + for (var i = 0; i < numberofComponentsToRemove; i++) + ReadNextVarInt(cache); + + if (strcturedComponentsToAdd.Count > 0) + item.Components = strcturedComponentsToAdd; + + return item; + } + /// /// Read a single item slot from a cache of bytes and remove it from the cache /// @@ -664,8 +695,10 @@ namespace MinecraftClient.Protocol.Handlers } } - return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch, + var entity = new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch, data); + entity.UUID = entityUUID; + return entity; } /// @@ -1021,20 +1054,44 @@ namespace MinecraftClient.Protocol.Handlers } } + private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4; + + private static double UnpackLpVec3(long packedAxis) + { + return Math.Min((double)(packedAxis & 32767L), 32766.0) * 2.0 / 32766.0 - 1.0; + } + /// - /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+). - /// Variable-length encoding: first byte 0 = zero vector; otherwise - /// 2 bytes + 4 bytes (6 total), plus an optional VarInt continuation. + /// Read and decode an LpVec3 (low-precision vec3) from the cache (1.21.9+). + /// Returned vector is expressed in blocks per tick. /// - public void ReadNextLpVec3(Queue cache) + public (double X, double Y, double Z) ReadNextLpVec3Values(Queue cache) { int first = ReadNextByte(cache); if (first == 0) - return; - ReadNextByte(cache); // second byte - ReadData(4, cache); // uint32 - if ((first & 4) == 4) // continuation bit set - ReadNextVarInt(cache); + return (0.0, 0.0, 0.0); + + int second = ReadNextByte(cache); + uint high = (uint)ReadNextInt(cache); + long packed = ((long)high << 16) | (long)(second << 8) | (uint)first; + + long scale = first & 3; + if (HasLpVec3Continuation(first)) + scale |= ((long)ReadNextVarInt(cache) & 0xFFFFFFFFL) << 2; + + return ( + UnpackLpVec3(packed >> 3) * scale, + UnpackLpVec3(packed >> 18) * scale, + UnpackLpVec3(packed >> 33) * scale + ); + } + + /// + /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it. + /// + public void ReadNextLpVec3(Queue cache) + { + ReadNextLpVec3Values(cache); } /// diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 15d20b71..6777200d 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -811,6 +811,11 @@ namespace MinecraftClient.Protocol.Handlers return false; //Currently not implemented } + public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll) + { + return false; //MC 1.8-1.12.1 recipe book not supported + } + public bool SendCloseWindow(int windowId) { return false; //Currently not implemented diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index a36eaf72..21d37c88 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -91,6 +91,7 @@ namespace MinecraftClient.Protocol.Handlers private int currentDimension; private bool isOnlineMode = false; private readonly BlockingCollection>> packetQueue = new(); + private readonly Dictionary legacyAchievementProgress = new(StringComparer.Ordinal); private float LastYaw, LastPitch; private double lastSentX, lastSentY, lastSentZ; private float lastSentYaw, lastSentPitch; @@ -120,6 +121,7 @@ namespace MinecraftClient.Protocol.Handlers Tuple? netReader = null; // reader thread readonly ILogger log; readonly RandomNumberGenerator randomGen; + private bool legacyAchievementsInitialized; public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler, ForgeInfo? forgeInfo, int rawProtocolVersion = 0) @@ -322,6 +324,12 @@ namespace MinecraftClient.Protocol.Handlers catch (NullReferenceException) { } + catch (SocketException) + { + } + catch (System.IO.IOException) + { + } if (cancelToken.IsCancellationRequested) return; @@ -449,7 +457,7 @@ namespace MinecraftClient.Protocol.Handlers McClient.Instance?.GetCookie(cookieName, out cookieData); SendCookieResponse(cookieName, cookieData); break; - + // Ignore other packets at this stage default: return true; @@ -467,7 +475,7 @@ namespace MinecraftClient.Protocol.Handlers McClient.Instance?.GetCookie(cookieName, out cookieData); SendCookieResponse(cookieName, cookieData); break; - + case ConfigurationPacketTypesIn.Disconnect: handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, dataTypes.ReadNextChat(packetData)); @@ -509,7 +517,7 @@ namespace MinecraftClient.Protocol.Handlers var dimensionIdMap = isDimension ? new Dictionary() : null; var attributeIdMap = isAttribute ? new Dictionary() : null; var enchantmentIdMap = isEnchantment ? new Dictionary() : null; - + for (var i = 0; i < entryCount; i++) { var entryId = dataTypes.ReadNextString(packetData); @@ -537,7 +545,7 @@ namespace MinecraftClient.Protocol.Handlers else if (isEnchantment) enchantmentIdMap!.Add(i, entryId); } - + if (isChat) ChatParser.ReadChatType(availableChats!); else if (isDimension) @@ -553,7 +561,7 @@ namespace MinecraftClient.Protocol.Handlers } break; - + case ConfigurationPacketTypesIn.RemoveResourcePack: if (dataTypes.ReadNextBool(packetData)) // Has UUID dataTypes.ReadNextUUID(packetData); // UUID @@ -562,24 +570,24 @@ namespace MinecraftClient.Protocol.Handlers case ConfigurationPacketTypesIn.ResourcePack: HandleResourcePackPacket(packetData); break; - + case ConfigurationPacketTypesIn.StoreCookie: var name = dataTypes.ReadNextString(packetData); var data = dataTypes.ReadNextByteArray(packetData); McClient.Instance?.SetCookie(name, data); break; - + case ConfigurationPacketTypesIn.Transfer: var host = dataTypes.ReadNextString(packetData); var port = dataTypes.ReadNextVarInt(packetData); - + McClient.Instance?.Transfer(host, port); break; - + case ConfigurationPacketTypesIn.KnownDataPacks: var knownPacksCount = dataTypes.ReadNextVarInt(packetData); List<(string, string, string)> knownDataPacks = new(); - + for (var i = 0; i < knownPacksCount; i++) { var nameSpace = dataTypes.ReadNextString(packetData); @@ -645,7 +653,7 @@ namespace MinecraftClient.Protocol.Handlers currentState == CurrentState.Login, innerException.GetType()), innerException); - + SentrySdk.AddBreadcrumb(new Breadcrumb("S -> C Packet", "network", new Dictionary() { { "Packet ID", packetId.ToString() }, @@ -786,26 +794,26 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { case >= MC_1_16_Version: - { - switch (protocolVersion) { - case >= MC_1_19_Version: - dimensionTypeName = - dataTypes.ReadNextString(packetData); // Dimension Type: Identifier - break; - case >= MC_1_16_2_Version: - dimensionType = - dataTypes.ReadNextNbt( - packetData); // Dimension Type: NBT Tag Compound - break; - default: - dataTypes.ReadNextString(packetData); - break; - } + switch (protocolVersion) + { + case >= MC_1_19_Version: + dimensionTypeName = + dataTypes.ReadNextString(packetData); // Dimension Type: Identifier + break; + case >= MC_1_16_2_Version: + dimensionType = + dataTypes.ReadNextNbt( + packetData); // Dimension Type: NBT Tag Compound + break; + default: + dataTypes.ReadNextString(packetData); + break; + } - currentDimension = 0; - break; - } + currentDimension = 0; + break; + } case >= MC_1_9_1_Version: currentDimension = dataTypes.ReadNextInt(packetData); break; @@ -820,27 +828,27 @@ namespace MinecraftClient.Protocol.Handlers dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below break; case >= MC_1_16_Version: - { - var dimensionName = - dataTypes.ReadNextString( - packetData); // Dimension Name (World Name) - 1.16 and above - - if (handler.GetTerrainEnabled()) { - switch (protocolVersion) - { - case >= MC_1_16_2_Version and <= MC_1_18_2_Version: - World.StoreOneDimension(dimensionName, dimensionType!); - World.SetDimension(dimensionName); - break; - default: - World.SetDimension(dimensionTypeName!); - break; - } - } + var dimensionName = + dataTypes.ReadNextString( + packetData); // Dimension Name (World Name) - 1.16 and above - break; - } + if (handler.GetTerrainEnabled()) + { + switch (protocolVersion) + { + case >= MC_1_16_2_Version and <= MC_1_18_2_Version: + World.StoreOneDimension(dimensionName, dimensionType!); + World.SetDimension(dimensionName); + break; + default: + World.SetDimension(dimensionTypeName!); + break; + } + } + + break; + } } } @@ -1354,7 +1362,7 @@ namespace MinecraftClient.Protocol.Handlers case PacketTypesIn.Respawn: string? dimensionTypeNameRespawn = null; Dictionary? dimensionTypeRespawn = null; - + if (protocolVersion >= MC_1_16_Version) { switch (protocolVersion) @@ -1386,27 +1394,27 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { case >= MC_1_16_Version: - { - var dimensionName = - dataTypes.ReadNextString( - packetData); // Dimension Name (World Name) - 1.16 and above - - if (handler.GetTerrainEnabled()) { - switch (protocolVersion) - { - case >= MC_1_16_2_Version and <= MC_1_18_2_Version: - World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); - World.SetDimension(dimensionName); - break; - default: - World.SetDimension(dimensionTypeNameRespawn!); - break; - } - } + var dimensionName = + dataTypes.ReadNextString( + packetData); // Dimension Name (World Name) - 1.16 and above - break; - } + if (handler.GetTerrainEnabled()) + { + switch (protocolVersion) + { + case >= MC_1_16_2_Version and <= MC_1_18_2_Version: + World.StoreOneDimension(dimensionName, dimensionTypeRespawn!); + World.SetDimension(dimensionName); + break; + default: + World.SetDimension(dimensionTypeNameRespawn!); + break; + } + } + + break; + } case < MC_1_14_Version: dataTypes.ReadNextByte(packetData); // Difficulty - 1.13 and below break; @@ -1453,77 +1461,77 @@ namespace MinecraftClient.Protocol.Handlers handler.OnRespawn(); break; case PacketTypesIn.PlayerPositionAndLook: - { - int teleportId; - Location location; - float yaw, pitch; - int locMask; + { + int teleportId; + Location location; + float yaw, pitch; + int locMask; - if (protocolVersion >= MC_1_21_2_Version) - { - teleportId = dataTypes.ReadNextVarInt(packetData); - location = new Location( - dataTypes.ReadNextDouble(packetData), // X - dataTypes.ReadNextDouble(packetData), // Y - dataTypes.ReadNextDouble(packetData) // Z - ); - dataTypes.ReadNextDouble(packetData); // Delta X - dataTypes.ReadNextDouble(packetData); // Delta Y - dataTypes.ReadNextDouble(packetData); // Delta Z - yaw = dataTypes.ReadNextFloat(packetData); - pitch = dataTypes.ReadNextFloat(packetData); - locMask = dataTypes.ReadNextInt(packetData); // Int flags (was Byte before 1.21.2) - } - else - { - location = new Location( - dataTypes.ReadNextDouble(packetData), // X - dataTypes.ReadNextDouble(packetData), // Y - dataTypes.ReadNextDouble(packetData) // Z - ); - yaw = dataTypes.ReadNextFloat(packetData); - pitch = dataTypes.ReadNextFloat(packetData); - locMask = dataTypes.ReadNextByte(packetData); - teleportId = protocolVersion >= MC_1_9_Version - ? dataTypes.ReadNextVarInt(packetData) : -1; - } - - if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) - { - if (protocolVersion >= MC_1_8_Version) + if (protocolVersion >= MC_1_21_2_Version) { - var currentLocation = handler.GetCurrentLocation(); - location.X = (locMask & 1 << 0) != 0 ? currentLocation.X + location.X : location.X; - location.Y = (locMask & 1 << 1) != 0 ? currentLocation.Y + location.Y : location.Y; - location.Z = (locMask & 1 << 2) != 0 ? currentLocation.Z + location.Z : location.Z; + teleportId = dataTypes.ReadNextVarInt(packetData); + location = new Location( + dataTypes.ReadNextDouble(packetData), // X + dataTypes.ReadNextDouble(packetData), // Y + dataTypes.ReadNextDouble(packetData) // Z + ); + dataTypes.ReadNextDouble(packetData); // Delta X + dataTypes.ReadNextDouble(packetData); // Delta Y + dataTypes.ReadNextDouble(packetData); // Delta Z + yaw = dataTypes.ReadNextFloat(packetData); + pitch = dataTypes.ReadNextFloat(packetData); + locMask = dataTypes.ReadNextInt(packetData); // Int flags (was Byte before 1.21.2) } - } - - if (teleportId >= 0) - { - LastYaw = yaw; - LastPitch = pitch; - handler.UpdateLocation(location, yaw, pitch); - SendPacket(PacketTypesOut.TeleportConfirm, DataTypes.GetVarInt(teleportId)); - - if (Config.Main.Advanced.TemporaryFixBadpacket) + else { - SendLocationUpdate(location, true, false, yaw, pitch, true); + location = new Location( + dataTypes.ReadNextDouble(packetData), // X + dataTypes.ReadNextDouble(packetData), // Y + dataTypes.ReadNextDouble(packetData) // Z + ); + yaw = dataTypes.ReadNextFloat(packetData); + pitch = dataTypes.ReadNextFloat(packetData); + locMask = dataTypes.ReadNextByte(packetData); + teleportId = protocolVersion >= MC_1_9_Version + ? dataTypes.ReadNextVarInt(packetData) : -1; + } - if (teleportId == 1) + if (handler.GetTerrainEnabled() || handler.GetEntityHandlingEnabled()) + { + if (protocolVersion >= MC_1_8_Version) + { + var currentLocation = handler.GetCurrentLocation(); + location.X = (locMask & 1 << 0) != 0 ? currentLocation.X + location.X : location.X; + location.Y = (locMask & 1 << 1) != 0 ? currentLocation.Y + location.Y : location.Y; + location.Z = (locMask & 1 << 2) != 0 ? currentLocation.Z + location.Z : location.Z; + } + } + + if (teleportId >= 0) + { + LastYaw = yaw; + LastPitch = pitch; + handler.UpdateLocation(location, yaw, pitch); + SendPacket(PacketTypesOut.TeleportConfirm, DataTypes.GetVarInt(teleportId)); + + if (Config.Main.Advanced.TemporaryFixBadpacket) + { SendLocationUpdate(location, true, false, yaw, pitch, true); - } - } - else - { - handler.UpdateLocation(location, yaw, pitch); - LastYaw = yaw; - LastPitch = pitch; - } - if (protocolVersion is >= MC_1_17_Version and < MC_1_19_4_Version) - dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 to 1.19.3 - } + if (teleportId == 1) + SendLocationUpdate(location, true, false, yaw, pitch, true); + } + } + else + { + handler.UpdateLocation(location, yaw, pitch); + LastYaw = yaw; + LastPitch = pitch; + } + + if (protocolVersion is >= MC_1_17_Version and < MC_1_19_4_Version) + dataTypes.ReadNextBool(packetData); // Dismount Vehicle - 1.17 to 1.19.3 + } break; case PacketTypesIn.ChunkData: if (handler.GetTerrainEnabled()) @@ -1680,26 +1688,26 @@ namespace MinecraftClient.Protocol.Handlers { // 1.8 - 1.13 case < MC_1_13_2_Version: - { - var directionAndType = dataTypes.ReadNextByte(packetData); - byte direction, type; - - // 1.12.2+ - if (protocolVersion >= MC_1_12_2_Version) { - direction = (byte)(directionAndType & 0xF); - type = (byte)(directionAndType >> 4 & 0xF); - } - else // 1.8 - 1.12 - { - direction = (byte)(directionAndType >> 4 & 0xF); - type = (byte)(directionAndType & 0xF); - } + var directionAndType = dataTypes.ReadNextByte(packetData); + byte direction, type; - mapIcon.Type = (MapIconType)type; - mapIcon.Direction = direction; - break; - } + // 1.12.2+ + if (protocolVersion >= MC_1_12_2_Version) + { + direction = (byte)(directionAndType & 0xF); + type = (byte)(directionAndType >> 4 & 0xF); + } + else // 1.8 - 1.12 + { + direction = (byte)(directionAndType >> 4 & 0xF); + type = (byte)(directionAndType & 0xF); + } + + mapIcon.Type = (MapIconType)type; + mapIcon.Direction = direction; + break; + } // 1.13.2+ case >= MC_1_13_2_Version: mapIcon.Type = (MapIconType)dataTypes.ReadNextVarInt(packetData); @@ -2291,7 +2299,7 @@ namespace MinecraftClient.Protocol.Handlers handler.OnPluginChannelMessage(channel, packetData.ToArray()); return pForge.HandlePluginMessage(channel, packetData, ref currentDimension); case PacketTypesIn.Disconnect: - handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, + handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, dataTypes.ReadNextChat(packetData)); return false; case PacketTypesIn.SetCompression: @@ -2428,17 +2436,17 @@ namespace MinecraftClient.Protocol.Handlers if (handler.GetEntityHandlingEnabled()) { var entity = dataTypes.ReadNextEntity(packetData, entityPalette, false); - + if (protocolVersion >= MC_1_20_2_Version) { if (entity.Type == EntityType.Player) handler.OnSpawnPlayer(entity.ID, entity.UUID, entity.Location, (byte)entity.Yaw, (byte)entity.Pitch); else handler.OnSpawnEntity(entity); - + break; } - + handler.OnSpawnEntity(entity); } @@ -2513,7 +2521,7 @@ namespace MinecraftClient.Protocol.Handlers if (handler.GetEntityHandlingEnabled()) { var entityId = dataTypes.ReadNextVarInt(packetData); - var effectId = protocolVersion >= MC_1_18_2_Version + var effectId = protocolVersion >= MC_1_20_4_Version ? dataTypes.ReadNextVarInt(packetData) + 1 : dataTypes.ReadNextByte(packetData); @@ -2543,7 +2551,7 @@ namespace MinecraftClient.Protocol.Handlers if (handler.GetEntityHandlingEnabled()) { var entityId = dataTypes.ReadNextVarInt(packetData); - var effectId = protocolVersion >= MC_1_18_2_Version + var effectId = protocolVersion >= MC_1_20_4_Version ? dataTypes.ReadNextVarInt(packetData) + 1 : dataTypes.ReadNextByte(packetData); @@ -2641,6 +2649,27 @@ namespace MinecraftClient.Protocol.Handlers handler.OnEntityRotation(entityId, yaw, pitch, isOnGround); } + break; + case PacketTypesIn.EntityVelocity: + if (handler.GetEntityHandlingEnabled()) + { + var entityId = dataTypes.ReadNextVarInt(packetData); + double velocityX, velocityY, velocityZ; + + if (protocolVersion >= MC_1_21_9_Version) + { + (velocityX, velocityY, velocityZ) = dataTypes.ReadNextLpVec3Values(packetData); + } + else + { + velocityX = dataTypes.ReadNextShort(packetData) / 8000.0D; + velocityY = dataTypes.ReadNextShort(packetData) / 8000.0D; + velocityZ = dataTypes.ReadNextShort(packetData) / 8000.0D; + } + + handler.OnEntityVelocity(entityId, velocityX, velocityY, velocityZ); + } + break; case PacketTypesIn.EntityProperties: if (handler.GetEntityHandlingEnabled()) @@ -2649,7 +2678,7 @@ namespace MinecraftClient.Protocol.Handlers var numberOfProperties = protocolVersion >= MC_1_17_Version ? dataTypes.ReadNextVarInt(packetData) : dataTypes.ReadNextInt(packetData); - + Dictionary keys = new(); for (var i = 0; i < numberOfProperties; i++) { @@ -2823,46 +2852,133 @@ namespace MinecraftClient.Protocol.Handlers break; case PacketTypesIn.Explosion: Location explosionLocation; - if (protocolVersion >= MC_1_19_3_Version) + float explosionStrength; + int explosionBlockCount; + + if (protocolVersion >= MC_1_21_2_Version) + { + // 1.21.2+: removed strength, block records, and player motion floats; + // added optional knockback (doubles) and single particle explosionLocation = new(dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData)); - else - explosionLocation = new(dataTypes.ReadNextFloat(packetData), - dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); + explosionStrength = 0; + explosionBlockCount = 0; - var explosionStrength = dataTypes.ReadNextFloat(packetData); - var explosionBlockCount = protocolVersion >= MC_1_17_Version - ? dataTypes.ReadNextVarInt(packetData) - : dataTypes.ReadNextInt(packetData); // Record count + if (dataTypes.ReadNextBool(packetData)) // Has player knockback + { + dataTypes.ReadNextDouble(packetData); // Knockback X + dataTypes.ReadNextDouble(packetData); // Knockback Y + dataTypes.ReadNextDouble(packetData); // Knockback Z + } - // Records - for (var i = 0; i < explosionBlockCount; i++) - dataTypes.ReadNextByteArray(packetData, 3); + dataTypes.ReadParticleData(packetData, itemPalette); // Explosion particle - dataTypes.ReadNextFloat(packetData); // Player Motion X - dataTypes.ReadNextFloat(packetData); // Player Motion Y - dataTypes.ReadNextFloat(packetData); // Player Motion Z - - if (protocolVersion >= MC_1_20_4_Version) - { - dataTypes.ReadNextVarInt(packetData); // Block Interaction (enum ordinal) - dataTypes.ReadParticleData(packetData, itemPalette); // Small Explosion Particles - dataTypes.ReadParticleData(packetData, itemPalette); // Large Explosion Particles - - // Explosion Sound: Holder via ByteBufCodecs.holder() - // VarInt id: 0 = inline (read DIRECT_STREAM_CODEC), >0 = registry ref (id-1) var soundHolderId = dataTypes.ReadNextVarInt(packetData); if (soundHolderId == 0) { dataTypes.ReadNextString(packetData); // Sound ResourceLocation - var hasFixedRange = dataTypes.ReadNextBool(packetData); - if (hasFixedRange) + if (dataTypes.ReadNextBool(packetData)) dataTypes.ReadNextFloat(packetData); // Fixed range } } + else + { + if (protocolVersion >= MC_1_19_3_Version) + explosionLocation = new(dataTypes.ReadNextDouble(packetData), + dataTypes.ReadNextDouble(packetData), dataTypes.ReadNextDouble(packetData)); + else + explosionLocation = new(dataTypes.ReadNextFloat(packetData), + dataTypes.ReadNextFloat(packetData), dataTypes.ReadNextFloat(packetData)); + + explosionStrength = dataTypes.ReadNextFloat(packetData); + explosionBlockCount = protocolVersion >= MC_1_17_Version + ? dataTypes.ReadNextVarInt(packetData) + : dataTypes.ReadNextInt(packetData); + + for (var i = 0; i < explosionBlockCount; i++) + dataTypes.ReadNextByteArray(packetData, 3); + + dataTypes.ReadNextFloat(packetData); // Player Motion X + dataTypes.ReadNextFloat(packetData); // Player Motion Y + dataTypes.ReadNextFloat(packetData); // Player Motion Z + + if (protocolVersion >= MC_1_20_4_Version) + { + dataTypes.ReadNextVarInt(packetData); // Block Interaction + dataTypes.ReadParticleData(packetData, itemPalette); // Small Explosion Particles + dataTypes.ReadParticleData(packetData, itemPalette); // Large Explosion Particles + + var soundHolderId = dataTypes.ReadNextVarInt(packetData); + if (soundHolderId == 0) + { + dataTypes.ReadNextString(packetData); // Sound ResourceLocation + if (dataTypes.ReadNextBool(packetData)) + dataTypes.ReadNextFloat(packetData); // Fixed range + } + } + } handler.OnExplosion(explosionLocation, explosionStrength, explosionBlockCount); break; + case PacketTypesIn.NamedSoundEffect: + { + string? soundName = dataTypes.ReadNextString(packetData); + int category = dataTypes.ReadNextVarInt(packetData); + double x = dataTypes.ReadNextInt(packetData) / 8.0D; + double y = dataTypes.ReadNextInt(packetData) / 8.0D; + double z = dataTypes.ReadNextInt(packetData) / 8.0D; + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + handler.OnSoundEffect(soundName, new Location(x, y, z), category, volume, pitch, null); + break; + } + case PacketTypesIn.SoundEffect: + { + string? soundName; + if (protocolVersion >= MC_1_19_Version) + soundName = ReadSoundEventHolderName(packetData); + else + { + dataTypes.ReadNextVarInt(packetData); // Sound id + soundName = null; + } + + int category = dataTypes.ReadNextVarInt(packetData); + double x = dataTypes.ReadNextInt(packetData) / 8.0D; + double y = dataTypes.ReadNextInt(packetData) / 8.0D; + double z = dataTypes.ReadNextInt(packetData) / 8.0D; + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + if (protocolVersion >= MC_1_19_Version) + dataTypes.ReadNextLong(packetData); // Seed + + handler.OnSoundEffect(soundName, new Location(x, y, z), category, volume, pitch, null); + break; + } + case PacketTypesIn.EntitySoundEffect: + { + string? soundName; + if (protocolVersion >= MC_1_19_Version) + soundName = ReadSoundEventHolderName(packetData); + else + { + dataTypes.ReadNextVarInt(packetData); // Sound id + soundName = null; + } + + int category = dataTypes.ReadNextVarInt(packetData); + int entityId = dataTypes.ReadNextVarInt(packetData); + float volume = dataTypes.ReadNextFloat(packetData); + float pitch = dataTypes.ReadNextFloat(packetData); + + if (protocolVersion >= MC_1_19_Version) + dataTypes.ReadNextLong(packetData); // Seed + + handler.OnSoundEffect(soundName, null, category, volume, pitch, entityId); + break; + } case PacketTypesIn.HeldItemChange: case PacketTypesIn.SetHeldSlot: handler.OnHeldItemChange(dataTypes.ReadNextByte(packetData)); // Slot @@ -2926,6 +3042,84 @@ namespace MinecraftClient.Protocol.Handlers handler.OnUpdateScore(entityName, action3, objectiveName3, objectiveDisplayName3, objectiveValue2, numberFormat2); break; + case PacketTypesIn.Teams: + // Wire format per version: + // All versions: name (string), method (byte) + // method 0/2: displayName (component), options (byte), + // nameTagVisibility, collisionRule, color (VarInt), + // prefix (component), suffix (component) + // method 0/3/4: players list (VarInt count + strings) + // 1.21.9+ (protocol 773): nameTagVisibility and collisionRule are + // VarInt-encoded enum IDs instead of UTF strings. + var teamName = dataTypes.ReadNextString(packetData); + var teamMethod = dataTypes.ReadNextByte(packetData); + + var teamDisplayName = string.Empty; + byte teamFriendlyFlags = 0; + var teamNameTagVisibility = string.Empty; + var teamCollisionRule = string.Empty; + var teamColor = -1; + var teamPrefix = string.Empty; + var teamSuffix = string.Empty; + + if (teamMethod is 0 or 2) + { + teamDisplayName = dataTypes.ReadNextChat(packetData); + teamFriendlyFlags = dataTypes.ReadNextByte(packetData); + + // nameTagVisibility + if (protocolVersion >= MC_1_21_9_Version) + { + // STREAM_CODEC: 0=always, 1=never, 2=hideForOtherTeams, 3=hideForOwnTeam + teamNameTagVisibility = dataTypes.ReadNextVarInt(packetData) switch + { + 0 => "always", + 1 => "never", + 2 => "hideForOtherTeams", + 3 => "hideForOwnTeam", + _ => "always" + }; + } + else + { + teamNameTagVisibility = dataTypes.ReadNextString(packetData); + } + + // collisionRule + if (protocolVersion >= MC_1_21_9_Version) + { + // STREAM_CODEC: 0=always, 1=never, 2=pushOtherTeams, 3=pushOwnTeam + teamCollisionRule = dataTypes.ReadNextVarInt(packetData) switch + { + 0 => "always", + 1 => "never", + 2 => "pushOtherTeams", + 3 => "pushOwnTeam", + _ => "always" + }; + } + else + { + teamCollisionRule = dataTypes.ReadNextString(packetData); + } + + teamColor = dataTypes.ReadNextVarInt(packetData); + teamPrefix = dataTypes.ReadNextChat(packetData); + teamSuffix = dataTypes.ReadNextChat(packetData); + } + + var teamPlayers = new List(); + if (teamMethod is 0 or 3 or 4) + { + int playerCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < playerCount; i++) + teamPlayers.Add(dataTypes.ReadNextString(packetData)); + } + + handler.OnTeam(teamName, teamMethod, teamDisplayName, teamFriendlyFlags, + teamNameTagVisibility, teamCollisionRule, teamColor, + teamPrefix, teamSuffix, teamPlayers); + break; case PacketTypesIn.BlockChangedAck: handler.OnBlockChangeAck(dataTypes.ReadNextVarInt(packetData)); break; @@ -2980,17 +3174,17 @@ namespace MinecraftClient.Protocol.Handlers McClient.Instance?.GetCookie(cookieName, out cookieData); SendCookieResponse(cookieName, cookieData); break; - + case PacketTypesIn.StoreCookie: var cookieName2 = dataTypes.ReadNextString(packetData); var cookieData2 = dataTypes.ReadNextByteArray(packetData); McClient.Instance?.SetCookie(cookieName2, cookieData2); break; - + case PacketTypesIn.Transfer: var host = dataTypes.ReadNextString(packetData); var port = dataTypes.ReadNextVarInt(packetData); - + McClient.Instance?.Transfer(host, port); break; @@ -3088,11 +3282,35 @@ namespace MinecraftClient.Protocol.Handlers } break; + case PacketTypesIn.UnlockRecipes: + if (protocolVersion >= MC_1_13_Version) + HandleUnlockRecipes(packetData); + break; + case PacketTypesIn.RecipeBookAdd: + if (protocolVersion >= MC_1_21_2_Version) + HandleRecipeBookAdd(packetData); + break; case PacketTypesIn.RecipeBookRemove: + if (protocolVersion >= MC_1_21_2_Version) + handler.OnRecipeBookRemove(ReadRecipeBookDisplayIds(packetData)); + break; case PacketTypesIn.RecipeBookSettings: break; + case PacketTypesIn.Statistics: + if (protocolVersion < MC_1_12_Version) + HandleLegacyStatistics(packetData); + break; + + case PacketTypesIn.Advancements: + HandleAdvancements(packetData); + break; + + case PacketTypesIn.SelectAdvancementTab: + HandleSelectAdvancementTab(packetData); + break; + default: return false; //Ignored packet } @@ -3100,6 +3318,527 @@ namespace MinecraftClient.Protocol.Handlers return true; //Packet processed } + /// + /// Read a Holder<SoundEvent> from packet data and return its key when inline. + /// Returns null when the holder is a registry reference. + /// + private string? ReadSoundEventHolderName(Queue packetData) + { + int soundHolderId = dataTypes.ReadNextVarInt(packetData); + if (soundHolderId != 0) + return null; + + string soundName = dataTypes.ReadNextString(packetData); + bool hasFixedRange = dataTypes.ReadNextBool(packetData); + if (hasFixedRange) + dataTypes.ReadNextFloat(packetData); + return soundName; + } + + /// + /// Handle the Statistics packet for pre-1.12 legacy achievements. + /// + private void HandleLegacyStatistics(Queue packetData) + { + int statCount = dataTypes.ReadNextVarInt(packetData); + + for (int i = 0; i < statCount; i++) + { + string statId = dataTypes.ReadNextString(packetData); + int value = dataTypes.ReadNextVarInt(packetData); + + if (statId.StartsWith("achievement.", StringComparison.Ordinal)) + legacyAchievementProgress[statId] = value > 0; + } + + List added = new(LegacyAchievementCatalog.Ids.Count + legacyAchievementProgress.Count); + + foreach (string achievementId in LegacyAchievementCatalog.Ids) + added.Add(CreateLegacyAchievement(achievementId, legacyAchievementProgress.TryGetValue(achievementId, out bool completed) && completed)); + + foreach (var (achievementId, completed) in legacyAchievementProgress) + { + if (!LegacyAchievementCatalog.Contains(achievementId)) + added.Add(CreateLegacyAchievement(achievementId, completed)); + } + + handler.OnAchievementsUpdate(added, [], reset: !legacyAchievementsInitialized); + legacyAchievementsInitialized = true; + } + + /// + /// Handle the Advancements packet (1.12+). + /// + private void HandleAdvancements(Queue packetData) + { + bool reset = dataTypes.ReadNextBool(packetData); + + // --- Added advancements --- + int addedCount = dataTypes.ReadNextVarInt(packetData); + var added = new List(addedCount); + var addedDefinitions = new Dictionary> requirements)>(addedCount); + + for (int i = 0; i < addedCount; i++) + { + string id = dataTypes.ReadNextString(packetData); + + // Parent + bool hasParent = dataTypes.ReadNextBool(packetData); + if (hasParent) + dataTypes.ReadNextString(packetData); // parentId - read and discard + + // Display + string? title = null; + string? description = null; + var type = AchievementType.Task; + bool isHidden = false; + + bool hasDisplay = dataTypes.ReadNextBool(packetData); + if (hasDisplay) + { + title = dataTypes.ReadNextChat(packetData); + description = dataTypes.ReadNextChat(packetData); + dataTypes.ReadNextItemSlot(packetData, itemPalette); // icon - read and discard + + int frameType = dataTypes.ReadNextVarInt(packetData); + type = frameType switch + { + 1 => AchievementType.Challenge, + 2 => AchievementType.Goal, + _ => AchievementType.Task + }; + + int flags = dataTypes.ReadNextInt(packetData); + isHidden = (flags & 0x04) != 0; + if ((flags & 0x01) != 0) + dataTypes.ReadNextString(packetData); // background texture - read and discard + + dataTypes.ReadNextFloat(packetData); // x + dataTypes.ReadNextFloat(packetData); // y + } + + // Criteria and requirements differ by version + var requirements = new List>(); + + if (protocolVersion < MC_1_20_2_Version) + { + // Builder-based (pre-1.20.2): criteria names list, then requirements + int criteriaCount = dataTypes.ReadNextVarInt(packetData); + for (int c = 0; c < criteriaCount; c++) + dataTypes.ReadNextString(packetData); // criterion name only, no trigger data + } + + // Requirements (all versions) + int reqGroupCount = dataTypes.ReadNextVarInt(packetData); + for (int g = 0; g < reqGroupCount; g++) + { + int groupSize = dataTypes.ReadNextVarInt(packetData); + var group = new List(groupSize); + for (int s = 0; s < groupSize; s++) + group.Add(dataTypes.ReadNextString(packetData)); + requirements.Add(group); + } + + // sendsTelemetryEvent (added in 1.20, present in all versions since) + if (protocolVersion >= MC_1_20_Version) + dataTypes.ReadNextBool(packetData); + + addedDefinitions[id] = (title, description, type, isHidden, requirements); + } + + // --- Removed advancement IDs --- + int removedCount = dataTypes.ReadNextVarInt(packetData); + var removedIds = new List(removedCount); + for (int i = 0; i < removedCount; i++) + removedIds.Add(dataTypes.ReadNextString(packetData)); + + // --- Progress updates --- + int progressCount = dataTypes.ReadNextVarInt(packetData); + var progressMap = new Dictionary>(progressCount); + + for (int i = 0; i < progressCount; i++) + { + string id = dataTypes.ReadNextString(packetData); + int criteriaEntries = dataTypes.ReadNextVarInt(packetData); + var criteria = new Dictionary(criteriaEntries); + + for (int c = 0; c < criteriaEntries; c++) + { + string criterionName = dataTypes.ReadNextString(packetData); + bool isDone = dataTypes.ReadNextBool(packetData); + if (isDone) + dataTypes.ReadNextLong(packetData); // epochMs - read and discard + criteria[criterionName] = isDone; + } + + progressMap[id] = criteria; + } + + // showAdvancements boolean added in 1.21.11+ + if (protocolVersion >= MC_1_21_11_Version) + dataTypes.ReadNextBool(packetData); // showAdvancements - read and discard + + // Build Achievement records from definitions + progress + foreach (var (id, def) in addedDefinitions) + { + progressMap.TryGetValue(id, out var criteria); + criteria ??= new Dictionary(); + + bool isCompleted = ComputeAdvancementCompleted(def.requirements, criteria); + + var readOnlyReqs = def.requirements.ConvertAll>(static g => g.AsReadOnly()); + added.Add(new Achievement(id, def.title, def.description, def.type, def.isHidden, isCompleted, readOnlyReqs.AsReadOnly(), criteria)); + } + + // Also build Achievement records for progress-only updates (no definition change) + foreach (var (id, criteria) in progressMap) + { + if (!addedDefinitions.ContainsKey(id)) + added.Add(new Achievement(id, null, null, AchievementType.Task, false, false, [], criteria)); + } + + handler.OnAchievementsUpdate(added, removedIds, reset); + } + + private static Achievement CreateLegacyAchievement(string id, bool isCompleted) + { + Dictionary criteria = new(StringComparer.Ordinal) + { + [id] = isCompleted + }; + IReadOnlyList[] requirements = [[id]]; + return new Achievement(id, null, null, AchievementType.Legacy, false, isCompleted, requirements, criteria); + } + + /// + /// Compute whether an advancement is completed based on AND-of-ORs requirements. + /// + private static bool ComputeAdvancementCompleted(List> requirements, Dictionary criteria) + { + // Zero requirements = automatically done + if (requirements.Count == 0) + return true; + + // Each OR-group must have at least one satisfied criterion + foreach (var group in requirements) + { + bool groupSatisfied = false; + foreach (string criterion in group) + { + if (criteria.TryGetValue(criterion, out bool done) && done) + { + groupSatisfied = true; + break; + } + } + if (!groupSatisfied) + return false; + } + return true; + } + + /// + /// Handle the SelectAdvancementTab packet. + /// + private void HandleSelectAdvancementTab(Queue packetData) + { + bool hasTab = dataTypes.ReadNextBool(packetData); + string? tabId = hasTab ? dataTypes.ReadNextString(packetData) : null; + handler.OnSelectAdvancementTab(tabId); + } + + private void HandleUnlockRecipes(Queue packetData) + { + int action = dataTypes.ReadNextVarInt(packetData); + if (!SkipRecipeBookSettings(packetData)) + return; + + string[] recipeIds = ReadRecipeBookRecipeIds(packetData); + RecipeBookRecipeEntry[] recipeEntries = recipeIds.Select(static recipeId => new RecipeBookRecipeEntry(recipeId, recipeId)).ToArray(); + + switch (action) + { + case 0: + handler.OnRecipeBookAdd(recipeEntries, replace: true); + // INIT packets also include a second "to be displayed" recipe list. + // MCC only needs the unlocked recipe identifiers for listing/crafting. + _ = ReadRecipeBookRecipeIds(packetData); + break; + case 1: + case 3: + // Action 3 is the silent-add variant, so MCC tracks it like a regular add. + handler.OnRecipeBookAdd(recipeEntries, replace: false); + break; + case 2: + handler.OnRecipeBookRemove(recipeIds); + break; + } + } + + private void HandleRecipeBookAdd(Queue packetData) + { + int entryCount = dataTypes.ReadNextVarInt(packetData); + RecipeBookRecipeEntry[] recipeEntries = new RecipeBookRecipeEntry[entryCount]; + + // 1.21.2+ RecipeBookAdd contains one display entry per recipe: + // RecipeDisplayEntry (display id, recipe display, group, category, optional requirements), then flags. + for (int i = 0; i < entryCount; i++) + { + recipeEntries[i] = ReadRecipeBookDisplayEntry(packetData); + _ = dataTypes.ReadNextByte(packetData); // flags + } + + bool replace = dataTypes.ReadNextBool(packetData); + handler.OnRecipeBookAdd(recipeEntries, replace); + } + + private string[] ReadRecipeBookRecipeIds(Queue packetData) + { + int recipeCount = dataTypes.ReadNextVarInt(packetData); + string[] recipeIds = new string[recipeCount]; + + for (int i = 0; i < recipeCount; i++) + recipeIds[i] = dataTypes.ReadNextString(packetData); + + return recipeIds; + } + + private string[] ReadRecipeBookDisplayIds(Queue packetData) + { + int recipeCount = dataTypes.ReadNextVarInt(packetData); + string[] recipeIds = new string[recipeCount]; + + for (int i = 0; i < recipeCount; i++) + recipeIds[i] = dataTypes.ReadNextVarInt(packetData).ToString(CultureInfo.InvariantCulture); + + return recipeIds; + } + + private RecipeBookRecipeEntry ReadRecipeBookDisplayEntry(Queue packetData) + { + int displayId = dataTypes.ReadNextVarInt(packetData); + string resultLabel = ReadRecipeDisplayResultLabel(packetData); + + _ = dataTypes.ReadNextVarInt(packetData); // Optional group, encoded as varint+1 or 0 + _ = dataTypes.ReadNextVarInt(packetData); // Recipe book category registry id + SkipOptionalCraftingRequirements(packetData); + + string commandId = displayId.ToString(CultureInfo.InvariantCulture); + string displayText = $"{commandId}: {resultLabel}"; + return new RecipeBookRecipeEntry(commandId, displayText); + } + + private string ReadRecipeDisplayResultLabel(Queue packetData) + { + int displayType = dataTypes.ReadNextVarInt(packetData); + return displayType switch + { + 0 => ReadShapelessRecipeDisplayResultLabel(packetData), + 1 => ReadShapedRecipeDisplayResultLabel(packetData), + 2 => ReadFurnaceRecipeDisplayResultLabel(packetData), + 3 => ReadStonecutterRecipeDisplayResultLabel(packetData), + 4 => ReadSmithingRecipeDisplayResultLabel(packetData), + _ => $"recipe_display_{displayType}", + }; + } + + private string ReadShapelessRecipeDisplayResultLabel(Queue packetData) + { + int ingredientCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < ingredientCount; i++) + _ = ReadSlotDisplayLabel(packetData); + + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadShapedRecipeDisplayResultLabel(Queue packetData) + { + _ = dataTypes.ReadNextVarInt(packetData); // width + _ = dataTypes.ReadNextVarInt(packetData); // height + int ingredientCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < ingredientCount; i++) + _ = ReadSlotDisplayLabel(packetData); + + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadFurnaceRecipeDisplayResultLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // ingredient + _ = ReadSlotDisplayLabel(packetData); // fuel + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + _ = dataTypes.ReadNextVarInt(packetData); // duration + _ = dataTypes.ReadNextFloat(packetData); // experience + return result; + } + + private string ReadStonecutterRecipeDisplayResultLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // input + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadSmithingRecipeDisplayResultLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // template + _ = ReadSlotDisplayLabel(packetData); // base + _ = ReadSlotDisplayLabel(packetData); // addition + string result = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // crafting station + return result; + } + + private string ReadSlotDisplayLabel(Queue packetData) + { + int slotDisplayType = dataTypes.ReadNextVarInt(packetData); + + // 26.1 changed the slot display registry order, inserting 3 new types: + // Pre-26.1: 0=empty, 1=any_fuel, 2=item, 3=item_stack, 4=tag, 5=smithing_trim, 6=with_remainder, 7=composite + // 26.1+: 0=empty, 1=any_fuel, 2=with_any_potion, 3=only_with_component, 4=item, 5=item_stack, 6=tag, 7=dyed, 8=smithing_trim, 9=with_remainder, 10=composite + if (protocolVersion >= MC_26_1_Version) + { + return slotDisplayType switch + { + 0 => "Empty", + 1 => "Any Fuel", + 2 => ReadWithAnyPotionSlotDisplayLabel(packetData), + 3 => ReadOnlyWithComponentSlotDisplayLabel(packetData), + 4 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))), + 5 => ReadItemStackTemplateLabel(packetData), + 6 => "#" + dataTypes.ReadNextString(packetData), + 7 => ReadDyedSlotDisplayLabel(packetData), + 8 => ReadSmithingTrimSlotDisplayLabel(packetData), + 9 => ReadWithRemainderSlotDisplayLabel(packetData), + 10 => ReadCompositeSlotDisplayLabel(packetData), + _ => $"slot_display_{slotDisplayType}", + }; + } + + return slotDisplayType switch + { + 0 => "Empty", + 1 => "Any Fuel", + 2 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))), + 3 => dataTypes.ReadNextItemSlot(packetData, itemPalette)?.GetTypeString() ?? "Empty", + 4 => "#" + dataTypes.ReadNextString(packetData), + 5 => ReadSmithingTrimSlotDisplayLabel(packetData), + 6 => ReadWithRemainderSlotDisplayLabel(packetData), + 7 => ReadCompositeSlotDisplayLabel(packetData), + _ => $"slot_display_{slotDisplayType}", + }; + } + + /// + /// Reads a with_any_potion slot display (26.1+): contains a nested SlotDisplay. + /// + private string ReadWithAnyPotionSlotDisplayLabel(Queue packetData) + { + return ReadSlotDisplayLabel(packetData); + } + + /// + /// Reads an only_with_component slot display (26.1+): contains a nested SlotDisplay and a DataComponentType VarInt ID. + /// + private string ReadOnlyWithComponentSlotDisplayLabel(Queue packetData) + { + string sourceLabel = ReadSlotDisplayLabel(packetData); + _ = dataTypes.ReadNextVarInt(packetData); // DataComponentType registry id + return sourceLabel; + } + + /// + /// Reads a dyed slot display (26.1+): contains two nested SlotDisplays (dye + target). + /// + private string ReadDyedSlotDisplayLabel(Queue packetData) + { + _ = ReadSlotDisplayLabel(packetData); // dye + string targetLabel = ReadSlotDisplayLabel(packetData); // target + return targetLabel; + } + + private string ReadSmithingTrimSlotDisplayLabel(Queue packetData) + { + string baseLabel = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // material + _ = dataTypes.ReadNextVarInt(packetData); // trim pattern registry id + return baseLabel; + } + + private string ReadWithRemainderSlotDisplayLabel(Queue packetData) + { + string inputLabel = ReadSlotDisplayLabel(packetData); + _ = ReadSlotDisplayLabel(packetData); // remainder + return inputLabel; + } + + private string ReadCompositeSlotDisplayLabel(Queue packetData) + { + int optionCount = dataTypes.ReadNextVarInt(packetData); + string label = "Composite"; + + for (int i = 0; i < optionCount; i++) + { + string optionLabel = ReadSlotDisplayLabel(packetData); + if (label == "Composite" && optionLabel is not "Empty" and not "Composite") + label = optionLabel; + } + + return label; + } + + /// + /// Read an ItemStackTemplate (26.1+) which encodes fields in a different order + /// than ItemStack: item_id (VarInt), count (VarInt), DataComponentPatch. + /// + private string ReadItemStackTemplateLabel(Queue packetData) + { + return dataTypes.ReadNextItemStackTemplate(packetData, itemPalette).GetTypeString(); + } + + private void SkipOptionalCraftingRequirements(Queue packetData) + { + if (!dataTypes.ReadNextBool(packetData)) + return; + + int ingredientCount = dataTypes.ReadNextVarInt(packetData); + for (int i = 0; i < ingredientCount; i++) + SkipItemHolderSet(packetData); + } + + private void SkipItemHolderSet(Queue packetData) + { + int entryCount = dataTypes.ReadNextVarInt(packetData) - 1; + if (entryCount == -1) + { + _ = dataTypes.ReadNextString(packetData); + return; + } + + for (int i = 0; i < entryCount; i++) + _ = dataTypes.ReadNextVarInt(packetData); + } + + private bool SkipRecipeBookSettings(Queue packetData) + { + // MC 1.13 uses 4 booleans for the crafting/smelting recipe book states. + // MC 1.14+ expands this to 8 booleans by adding blast furnace and smoker states. + int boolCount = protocolVersion >= MC_1_14_Version ? 8 : 4; + if (packetData.Count < boolCount) + return false; + + for (int i = 0; i < boolCount; i++) + _ = dataTypes.ReadNextBool(packetData); + + return true; + } + /// /// Start the updating thread. Should be called after login success. /// @@ -3197,26 +3936,42 @@ namespace MinecraftClient.Protocol.Handlers /// packet Data private void SendPacket(int packetId, IEnumerable packetData) { + byte[] payload = packetData as byte[] ?? packetData.ToArray(); + if (handler.GetNetworkPacketCaptureEnabled()) { - var clone = packetData.ToList(); - handler.OnNetworkPacket(packetId, clone, currentState == CurrentState.Login, false); + handler.OnNetworkPacket(packetId, payload.ToList(), currentState == CurrentState.Login, false); } //log.Info($"[C -> S] Sending packet {packetId:X} > {dataTypes.ByteArrayToString(packetData.ToArray())}"); //The inner packet - var thePacket = dataTypes.ConcatBytes(DataTypes.GetVarInt(packetId), packetData.ToArray()); + byte[] packetIdBytes = DataTypes.GetVarInt(packetId); + byte[] thePacket = new byte[packetIdBytes.Length + payload.Length]; + Buffer.BlockCopy(packetIdBytes, 0, thePacket, 0, packetIdBytes.Length); + Buffer.BlockCopy(payload, 0, thePacket, packetIdBytes.Length, payload.Length); if (compression_treshold >= 0) //Compression enabled? { - thePacket = thePacket.Length >= compression_treshold - ? dataTypes.ConcatBytes(DataTypes.GetVarInt(thePacket.Length), ZlibUtils.Compress(thePacket)) - : dataTypes.ConcatBytes(DataTypes.GetVarInt(0), thePacket); + byte[] compressedHeader = thePacket.Length >= compression_treshold + ? DataTypes.GetVarInt(thePacket.Length) + : DataTypes.GetVarInt(0); + byte[] compressedPayload = thePacket.Length >= compression_treshold + ? ZlibUtils.Compress(thePacket) + : thePacket; + + byte[] compressedPacket = new byte[compressedHeader.Length + compressedPayload.Length]; + Buffer.BlockCopy(compressedHeader, 0, compressedPacket, 0, compressedHeader.Length); + Buffer.BlockCopy(compressedPayload, 0, compressedPacket, compressedHeader.Length, compressedPayload.Length); + thePacket = compressedPacket; } //log.Debug("[C -> S] Sending packet " + packetId + " > " + dataTypes.ByteArrayToString(dataTypes.ConcatBytes(dataTypes.GetVarInt(thePacket.Length), thePacket))); - socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(thePacket.Length), thePacket)); + byte[] packetLengthBytes = DataTypes.GetVarInt(thePacket.Length); + byte[] fullPacket = new byte[packetLengthBytes.Length + thePacket.Length]; + Buffer.BlockCopy(packetLengthBytes, 0, fullPacket, 0, packetLengthBytes.Length); + Buffer.BlockCopy(thePacket, 0, fullPacket, packetLengthBytes.Length, thePacket.Length); + socketWrapper.SendDataRAW(fullPacket); } /// @@ -3276,17 +4031,17 @@ namespace MinecraftClient.Protocol.Handlers switch (protocolVersion) { case >= MC_1_19_2_Version and < MC_1_20_2_Version: - { - if (uuid == Guid.Empty) - fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID - else { - fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has UUID - fullLoginPacket.AddRange(DataTypes.GetUUID(uuid)); // UUID - } + if (uuid == Guid.Empty) + fullLoginPacket.AddRange(dataTypes.GetBool(false)); // Has UUID + else + { + fullLoginPacket.AddRange(dataTypes.GetBool(true)); // Has UUID + fullLoginPacket.AddRange(DataTypes.GetUUID(uuid)); // UUID + } - break; - } + break; + } case >= MC_1_20_2_Version: uuid = handler.GetUserUuid(); @@ -3314,42 +4069,42 @@ namespace MinecraftClient.Protocol.Handlers // Encryption request case 0x01: - { - isOnlineMode = true; - var serverId = dataTypes.ReadNextString(packetData); - var serverPublicKey = dataTypes.ReadNextByteArray(packetData); - var token = dataTypes.ReadNextByteArray(packetData); + { + isOnlineMode = true; + var serverId = dataTypes.ReadNextString(packetData); + var serverPublicKey = dataTypes.ReadNextByteArray(packetData); + var token = dataTypes.ReadNextByteArray(packetData); - var shouldAuthetnicate = false; + var shouldAuthetnicate = false; - if (protocolVersion >= MC_1_20_6_Version) - shouldAuthetnicate = dataTypes.ReadNextBool(packetData); - - return StartEncryption(handler.GetUserUuidStr(), handler.GetSessionID(), - Config.Main.General.AccountType, token, serverId, - serverPublicKey, playerKeyPair, session, shouldAuthetnicate); - } + if (protocolVersion >= MC_1_20_6_Version) + shouldAuthetnicate = dataTypes.ReadNextBool(packetData); + + return StartEncryption(handler.GetUserUuidStr(), handler.GetSessionID(), + Config.Main.General.AccountType, token, serverId, + serverPublicKey, playerKeyPair, session, shouldAuthetnicate); + } // Login successful case 0x02: - { - log.Info($"§8{Translations.mcc_server_offline}"); - currentState = protocolVersion < MC_1_20_2_Version - ? CurrentState.Play - : CurrentState.Configuration; - - if (protocolVersion >= MC_1_20_2_Version) - SendPacket(0x03, new List()); - - if (!pForge.CompleteForgeHandshake()) { - log.Error($"§8{Translations.error_forge}"); - return false; - } + log.Info($"§8{Translations.mcc_server_offline}"); + currentState = protocolVersion < MC_1_20_2_Version + ? CurrentState.Play + : CurrentState.Configuration; - StartUpdating(); - return true; //No need to check session or start encryption - } + if (protocolVersion >= MC_1_20_2_Version) + SendPacket(0x03, new List()); + + if (!pForge.CompleteForgeHandshake()) + { + log.Error($"§8{Translations.error_forge}"); + return false; + } + + StartUpdating(); + return true; //No need to check session or start encryption + } default: HandlePacket(packetId, packetData); break; @@ -3382,7 +4137,7 @@ namespace MinecraftClient.Protocol.Handlers if (session.SessionPreCheckTask.Result) // PreCheck Success needCheckSession = false; } - + // 1.20.6++ if (shouldAuthetnicate) needCheckSession = true; @@ -3455,51 +4210,51 @@ namespace MinecraftClient.Protocol.Handlers handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData))); return false; - + //Login successful case 0x02: - { - var uuidReceived = protocolVersion >= MC_1_16_Version - ? dataTypes.ReadNextUUID(packetData) - : Guid.Parse(dataTypes.ReadNextString(packetData)); - var userName = dataTypes.ReadNextString(packetData); - Tuple[]? playerProperty = null; - if (protocolVersion >= MC_1_19_Version) { - var count = dataTypes.ReadNextVarInt(packetData); // Number Of Properties - playerProperty = new Tuple[count]; - for (var i = 0; i < count; ++i) + var uuidReceived = protocolVersion >= MC_1_16_Version + ? dataTypes.ReadNextUUID(packetData) + : Guid.Parse(dataTypes.ReadNextString(packetData)); + var userName = dataTypes.ReadNextString(packetData); + Tuple[]? playerProperty = null; + if (protocolVersion >= MC_1_19_Version) { - var name = dataTypes.ReadNextString(packetData); - var value = dataTypes.ReadNextString(packetData); - var isSigned = dataTypes.ReadNextBool(packetData); - var signature = isSigned ? dataTypes.ReadNextString(packetData) : string.Empty; - playerProperty[i] = new Tuple(name, value, signature); + var count = dataTypes.ReadNextVarInt(packetData); // Number Of Properties + playerProperty = new Tuple[count]; + for (var i = 0; i < count; ++i) + { + var name = dataTypes.ReadNextString(packetData); + var value = dataTypes.ReadNextString(packetData); + var isSigned = dataTypes.ReadNextBool(packetData); + var signature = isSigned ? dataTypes.ReadNextString(packetData) : string.Empty; + playerProperty[i] = new Tuple(name, value, signature); + } } + + // Strict Error Handling (removed in 1.21.2) + if (protocolVersion >= MC_1_20_6_Version && protocolVersion < MC_1_21_2_Version) + dataTypes.ReadNextBool(packetData); + + currentState = protocolVersion < MC_1_20_2_Version + ? CurrentState.Play + : CurrentState.Configuration; + + if (protocolVersion >= MC_1_20_2_Version) + SendPacket(0x03, new List()); + + handler.OnLoginSuccess(uuidReceived, userName, playerProperty); + + if (!pForge.CompleteForgeHandshake()) + { + log.Error($"§8{Translations.error_forge_encrypt}"); + return false; + } + + StartUpdating(); + return true; } - - // Strict Error Handling (removed in 1.21.2) - if (protocolVersion >= MC_1_20_6_Version && protocolVersion < MC_1_21_2_Version) - dataTypes.ReadNextBool(packetData); - - currentState = protocolVersion < MC_1_20_2_Version - ? CurrentState.Play - : CurrentState.Configuration; - - if (protocolVersion >= MC_1_20_2_Version) - SendPacket(0x03, new List()); - - handler.OnLoginSuccess(uuidReceived, userName, playerProperty); - - if (!pForge.CompleteForgeHandshake()) - { - log.Error($"§8{Translations.error_forge_encrypt}"); - return false; - } - - StartUpdating(); - return true; - } default: HandlePacket(packetId, packetData); break; @@ -3538,15 +4293,15 @@ namespace MinecraftClient.Protocol.Handlers dataTypes.GetString(BehindCursor.Replace(' ', (char)0x00))); break; case >= MC_1_8_Version: - { - tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, dataTypes.GetString(BehindCursor)); + { + tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, dataTypes.GetString(BehindCursor)); - if (protocolVersion >= MC_1_9_Version) - tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, assumeCommand); + if (protocolVersion >= MC_1_9_Version) + tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, assumeCommand); - tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, hasPosition); - break; - } + tabCompletePacket = dataTypes.ConcatBytes(tabCompletePacket, hasPosition); + break; + } default: tabCompletePacket = dataTypes.ConcatBytes(dataTypes.GetString(BehindCursor)); break; @@ -3610,7 +4365,8 @@ namespace MinecraftClient.Protocol.Handlers if (dataTypes.ReadNextVarInt(packetData) != 0x00) return false; - var result = dataTypes.ReadNextString(packetData); // Get the Json data + // Get the Json data + var result = dataTypes.ReadNextString(packetData); if (Config.Logging.DebugMessages) { @@ -3641,7 +4397,44 @@ namespace MinecraftClient.Protocol.Handlers // Check for forge on the server. Protocol18Forge.ServerInfoCheckForge(jsonObj, ref forgeInfo); - // Complete the normal status exchange so the probe connection closes cleanly server-side. + int onlinePlayers = 0, maxPlayers = 0; + List samplePlayers = []; + + if (jsonObj["players"] is System.Text.Json.Nodes.JsonObject playersObj) + { + if (playersObj["online"] is { } onlineNode) + onlinePlayers = int.Parse(onlineNode.GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture); + if (playersObj["max"] is { } maxNode) + maxPlayers = int.Parse(maxNode.GetStringValue(), NumberStyles.Any, CultureInfo.CurrentCulture); + if (playersObj["sample"] is System.Text.Json.Nodes.JsonArray sampleArray) + { + foreach (var entry in sampleArray) + { + if (entry is not System.Text.Json.Nodes.JsonObject playerObj) continue; + samplePlayers.Add(new ServerStatusInfo.SamplePlayer + { + Name = playerObj["name"]?.GetStringValue() ?? "", + Id = playerObj["id"]?.GetStringValue() ?? "" + }); + } + } + } + + string motdRaw = ""; + if (jsonObj["description"] is { } descNode) + motdRaw = descNode.ToJsonString(); + + string? faviconBase64 = null; + if (jsonObj["favicon"] is { } faviconNode) + { + var faviconStr = faviconNode.GetStringValue(); + const string prefix = "data:image/png;base64,"; + faviconBase64 = faviconStr.StartsWith(prefix, StringComparison.Ordinal) + ? faviconStr[prefix.Length..] + : faviconStr; + } + + long pingMs = -1; try { long pingPayload = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -3653,7 +4446,10 @@ namespace MinecraftClient.Protocol.Handlers { packetData = new Queue(socketWrapper.ReadDataRAW(packetLength)); if (dataTypes.ReadNextVarInt(packetData) == 0x01) - dataTypes.ReadNextLong(packetData); + { + long pongPayload = dataTypes.ReadNextLong(packetData); + pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload; + } } } catch @@ -3661,9 +4457,28 @@ namespace MinecraftClient.Protocol.Handlers // Some servers may close the probe connection immediately after the status response. } + var statusInfo = new ServerStatusInfo + { + Host = host, + Port = port, + VersionName = version, + ProtocolVersion = protocolVersion, + OnlinePlayers = onlinePlayers, + MaxPlayers = maxPlayers, + SamplePlayers = samplePlayers, + MotdRaw = motdRaw, + FaviconBase64 = faviconBase64, + PingMs = pingMs + }; + + ProtocolHandler.TryUpgradeProtocolVersion(version, ref protocolVersion); + statusInfo.ResolvedProtocol = protocolVersion; + ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_server_protocol, version, protocolVersion + (forgeInfo is not null ? Translations.mcc_with_forge : ""))); + ServerStatusDisplay.Show(statusInfo); + return true; } finally @@ -3782,7 +4597,7 @@ namespace MinecraftClient.Protocol.Handlers SendMessageAcknowledgment(ConsumeAcknowledgment()); } } - + /// /// Send a chat command to the server, with or without signing based on the online mode and version. /// @@ -3907,7 +4722,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + /// /// Send a chat message to the server /// @@ -4531,7 +5346,7 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(dataTypes.GetFloat(LastYaw)); packet.AddRange(dataTypes.GetFloat(LastPitch)); } - + SendPacket(PacketTypesOut.UseItem, packet); return true; } @@ -4594,12 +5409,12 @@ namespace MinecraftClient.Protocol.Handlers if (playerInventory?.Items is null) return false; - int[] slotWindowIds = [36, 37, 38, 39, 40, 41, 42, 43, 44]; + int[] slotWindowIds = [36, 37, 38, 39, 40, 41, 42, 43, 44]; var currentSlot = ((McClient)handler).GetCurrentSlot(); - + playerInventory.Items.TryGetValue(slotWindowIds[currentSlot], out var item); packet.AddRange(dataTypes.GetItemSlot(item, itemPalette)); - + packet.Add(0); // cursorX packet.Add(0); // cursorY packet.Add(0); // cursorZ @@ -4616,12 +5431,12 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(DataTypes.GetVarInt(dataTypes.GetBlockFace(face))); break; } - + packet.AddRange(dataTypes.GetFloat(cursorX)); // cursorX packet.AddRange(dataTypes.GetFloat(cursorY)); // cursorY packet.AddRange(dataTypes.GetFloat(cursorZ)); // cursorZ - - if(protocolVersion >= MC_1_14_Version) + + if (protocolVersion >= MC_1_14_Version) packet.Add(0); // insideBlock = false if (protocolVersion >= MC_1_21_2_Version) @@ -4629,7 +5444,7 @@ namespace MinecraftClient.Protocol.Handlers if (protocolVersion >= MC_1_19_Version) packet.AddRange(DataTypes.GetVarInt(sequenceId)); - + SendPacket(PacketTypesOut.PlayerBlockPlacement, packet); return true; } @@ -4948,6 +5763,37 @@ namespace MinecraftClient.Protocol.Handlers } } + public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll) + { + try + { + List packet = new(); + if (protocolVersion < MC_1_13_Version) + return false; + + packet.AddRange(DataTypes.GetVarInt(windowId)); + if (protocolVersion >= MC_1_21_2_Version) + packet.AddRange(DataTypes.GetVarInt(int.Parse(recipeId, CultureInfo.InvariantCulture))); + else + packet.AddRange(dataTypes.GetString(recipeId)); + packet.AddRange(dataTypes.GetBool(makeAll)); + SendPacket(PacketTypesOut.CraftRecipeRequest, packet); + return true; + } + catch (SocketException) + { + return false; + } + catch (System.IO.IOException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + public bool SendAnimation(int animation, int playerId) { try @@ -5225,7 +6071,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + public bool SendCookieResponse(string name, byte[]? data) { try @@ -5234,7 +6080,7 @@ namespace MinecraftClient.Protocol.Handlers var hasPayload = data is not null; packet.AddRange(dataTypes.GetString(name)); // Identifier packet.AddRange(dataTypes.GetBool(hasPayload)); // Has payload - + if (hasPayload) packet.AddRange(dataTypes.GetArray(data!)); // Payload Data Array Size + Data Array @@ -5252,7 +6098,7 @@ namespace MinecraftClient.Protocol.Handlers SendPacket(PacketTypesOut.CookieResponse, packet); break; } - + McClient.Instance?.DeleteCookie(name); return true; } @@ -5283,17 +6129,17 @@ namespace MinecraftClient.Protocol.Handlers packet.AddRange(dataTypes.GetString(dataPack.Item3)); } - switch(currentState) + switch (currentState) { - case CurrentState.Configuration: + case CurrentState.Configuration: SendPacket(ConfigurationPacketTypesOut.KnownDataPacks, packet); break; - + case CurrentState.Play: SendPacket(PacketTypesOut.KnownDataPacks, packet); break; } - + return true; } catch (SocketException) @@ -5309,7 +6155,7 @@ namespace MinecraftClient.Protocol.Handlers return false; } } - + private byte[] GenerateSalt() { var salt = new byte[8]; diff --git a/MinecraftClient/Protocol/IMinecraftCom.cs b/MinecraftClient/Protocol/IMinecraftCom.cs index 6c7dd596..96b261b1 100644 --- a/MinecraftClient/Protocol/IMinecraftCom.cs +++ b/MinecraftClient/Protocol/IMinecraftCom.cs @@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol bool ClickContainerButton(int windowId, int buttonId); + /// + /// Send a place recipe packet to the server for the active recipe book container. + /// + /// Id of the window being clicked + /// Recipe identifier to craft + /// True to craft as many items as possible + /// True if packet was successfully sent + bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll); + /// /// Plays animation /// diff --git a/MinecraftClient/Protocol/IMinecraftComHandler.cs b/MinecraftClient/Protocol/IMinecraftComHandler.cs index 85618c07..5c100981 100644 --- a/MinecraftClient/Protocol/IMinecraftComHandler.cs +++ b/MinecraftClient/Protocol/IMinecraftComHandler.cs @@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol /// TRUE if on ground void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround); + /// + /// Called when an entity velocity update packet is received. + /// Velocity values are in blocks per tick. + /// + /// Entity ID + /// Velocity X + /// Velocity Y + /// Velocity Z + void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ); + /// /// Called when additional properties have been received for an entity /// @@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol /// Amount of affected blocks void OnExplosion(Location location, float strength, int affectedBlocks); + /// + /// Called when a sound packet is received. + /// + /// Sound key if available, otherwise null + /// Sound location for world sounds, or null if unavailable + /// Sound category id + /// Sound volume + /// Sound pitch + /// Source entity id for entity-sound packets, if any + void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID); + /// /// Called when a player's game mode has changed /// @@ -468,6 +489,23 @@ namespace MinecraftClient.Protocol /// Number format: 0 - blank, 1 - styled, 2 - fixed void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat); + /// + /// Called when a Teams packet is received from the server. + /// + /// Internal team name (up to 16 chars) + /// 0=create, 1=remove, 2=update, 3=add players, 4=remove players + /// Display name (formatted). Present when method is 0 or 2. + /// Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2. + /// Nametag visibility rule string. Present when method is 0 or 2. + /// Collision rule string. Present when method is 0 or 2. + /// ChatFormatting color value (-1=none). Present when method is 0 or 2. + /// Member name prefix (formatted). Present when method is 0 or 2. + /// Member name suffix (formatted). Present when method is 0 or 2. + /// Player/entity names. Present when method is 0, 3, or 4. + void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags, + string nameTagVisibility, string collisionRule, int color, + string prefix, string suffix, List players); + /// /// Called when the client received the Tab Header and Footer /// @@ -524,6 +562,33 @@ namespace MinecraftClient.Protocol public void SetCanSendMessage(bool canSendMessage); + /// + /// Called when recipe book recipes are added or replaced. + /// + /// Recipe entries to add + /// True to replace the currently tracked recipe book entries + public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace); + + /// + /// Called when recipe book recipes are removed. + /// + /// Recipe identifiers to remove + public void OnRecipeBookRemove(string[] recipeIds); + + /// + /// Called when achievement/advancement data is received from the server. + /// + /// Achievements that were added or updated + /// IDs of achievements that were removed + /// True if all existing state should be cleared before applying + public void OnAchievementsUpdate(IReadOnlyList added, IReadOnlyList removedIds, bool reset); + + /// + /// Called when the server selects an advancement tab. + /// + /// The tab identifier, or null if no tab is selected + public void OnSelectAdvancementTab(string? tabId); + /// /// Send a click container button packet to the server. /// Used for Enchanting table, Lectern, stone cutter and loom diff --git a/MinecraftClient/Protocol/ProtocolHandler.cs b/MinecraftClient/Protocol/ProtocolHandler.cs index c000385e..a8568f38 100644 --- a/MinecraftClient/Protocol/ProtocolHandler.cs +++ b/MinecraftClient/Protocol/ProtocolHandler.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Net.Sockets; using System.Text; +using System.Text.RegularExpressions; using DnsClient; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Handlers.Forge; @@ -388,6 +389,61 @@ namespace MinecraftClient.Protocol } } + private static readonly Regex VersionTokenRegex = new(@"\d+\.\d+(?:\.\d+)?", RegexOptions.Compiled); + + private static readonly int[] SupportedProtocols18 = + [ + 4, 5, 47, 107, 108, 109, 110, 210, 315, 316, 335, 338, 340, 393, 401, 404, + 477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756, + 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, + 772, 773, 774, 775 + ]; + + /// + /// For multi-version servers (e.g. "Requires MC 1.8 / 1.21"), try to find the + /// highest protocol version that both the server and MCC support. + /// Returns true if the protocol was upgraded, with the new value in + /// . + /// + public static bool TryUpgradeProtocolVersion(string versionName, ref int protocolVersion) + { + if (string.IsNullOrEmpty(versionName)) + return false; + + var matches = VersionTokenRegex.Matches(versionName); + if (matches.Count < 2) + return false; + + int bestProtocol = protocolVersion; + string bestVersion = ""; + + foreach (Match m in matches) + { + int proto = MCVer2ProtocolVersion(m.Value); + if (proto <= 0) + continue; + if (Array.IndexOf(SupportedProtocols18, proto) < 0) + continue; + if (proto > bestProtocol) + { + bestProtocol = proto; + bestVersion = m.Value; + } + } + + if (bestProtocol > protocolVersion && bestVersion.Length > 0) + { + ConsoleIO.WriteLineFormatted("§8" + string.Format( + Translations.mcc_server_info_version_upgrade, + ProtocolVersion2MCVer(protocolVersion), protocolVersion, + "§a" + bestVersion + "§8", bestProtocol)); + protocolVersion = bestProtocol; + return true; + } + + return false; + } + /// /// Convert a network protocol version number to human-readable Minecraft version number /// diff --git a/MinecraftClient/Protocol/ServerStatusDisplay.cs b/MinecraftClient/Protocol/ServerStatusDisplay.cs new file mode 100644 index 00000000..291745e5 --- /dev/null +++ b/MinecraftClient/Protocol/ServerStatusDisplay.cs @@ -0,0 +1,119 @@ +using System; +using System.Text; +using MinecraftClient.Protocol.Message; +using MinecraftClient.Scripting; + +namespace MinecraftClient.Protocol +{ + internal static class ServerStatusDisplay + { + private const int MaxSamplePlayers = 10; + + internal static void Show(ServerStatusInfo info) + { + if (ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBackend) + ShowTui(info, tuiBackend); + else + ShowClassic(info); + } + + private static void ShowClassic(ServerStatusInfo info) + { + var sb = new StringBuilder(); + + sb.AppendLine(); + sb.Append("§8§m"); + sb.Append(new string('-', 50)); + sb.AppendLine("§r"); + + if (!string.IsNullOrEmpty(info.MotdRaw)) + { + try + { + sb.AppendLine(ChatParser.ParseText(info.MotdRaw)); + } + catch + { + sb.AppendLine(info.MotdRaw); + } + } + + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_server); + sb.Append(" §b"); + sb.Append(info.Host); + sb.Append("§7:§b"); + sb.AppendLine(info.Port.ToString()); + + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_version); + sb.Append(" §b"); + sb.Append(ChatBot.GetVerbatim(info.VersionName)); + sb.Append(" §7("); + sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§e" + info.ProtocolVersion + "§7")); + sb.AppendLine(")"); + + if (info.ResolvedProtocol != 0) + { + string resolvedMcVer = ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol); + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_connecting_as); + sb.Append(" §a"); + sb.Append(resolvedMcVer); + sb.Append(" §7("); + sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§a" + info.ResolvedProtocol + "§7")); + sb.AppendLine(")"); + } + + if (info.PingMs >= 0) + { + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_ping); + sb.Append(" §a"); + sb.AppendLine(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs)); + } + + sb.Append("§f"); + sb.Append(Translations.mcc_server_info_label_players); + sb.Append(" §a"); + sb.Append(info.OnlinePlayers); + sb.Append("§7/§c"); + sb.AppendLine(info.MaxPlayers.ToString()); + + if (info.SamplePlayers.Count > 0) + { + sb.Append("§f"); + sb.AppendLine(Translations.mcc_server_info_label_online); + + int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers); + for (int i = 0; i < shown; i++) + sb.AppendLine($" §a{info.SamplePlayers[i].Name}"); + + if (info.SamplePlayers.Count > shown) + sb.AppendLine($" §7{string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}"); + } + + sb.Append("§8§m"); + sb.Append(new string('-', 50)); + sb.Append("§r"); + + ConsoleIO.WriteLineFormatted(sb.ToString(), acceptnewlines: true); + } + + private static void ShowTui(ServerStatusInfo info, Tui.TuiConsoleBackend backend) + { + var view = backend.GetView(); + if (view is null) + { + ShowClassic(info); + return; + } + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + var panel = Tui.ServerStatusPanelBuilder.Build(info); + view.AppendControlToLog(panel); + }); + } + } +} diff --git a/MinecraftClient/Protocol/ServerStatusInfo.cs b/MinecraftClient/Protocol/ServerStatusInfo.cs new file mode 100644 index 00000000..b864e64d --- /dev/null +++ b/MinecraftClient/Protocol/ServerStatusInfo.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace MinecraftClient.Protocol +{ + /// + /// Holds the structured result of a Minecraft server status (SLP) ping, + /// including MOTD, player counts, sample player list, version, and favicon. + /// + public sealed class ServerStatusInfo + { + public string Host { get; init; } = string.Empty; + public int Port { get; init; } + public string VersionName { get; init; } = string.Empty; + public int ProtocolVersion { get; init; } + public int ResolvedProtocol { get; set; } + public int OnlinePlayers { get; init; } + public int MaxPlayers { get; init; } + public List SamplePlayers { get; init; } = []; + public string MotdRaw { get; init; } = string.Empty; + public string? FaviconBase64 { get; init; } + public long PingMs { get; init; } + + public sealed class SamplePlayer + { + public string Name { get; init; } = string.Empty; + public string Id { get; init; } = string.Empty; + } + } +} diff --git a/MinecraftClient/RecipeBookRecipeEntry.cs b/MinecraftClient/RecipeBookRecipeEntry.cs new file mode 100644 index 00000000..a5648ba7 --- /dev/null +++ b/MinecraftClient/RecipeBookRecipeEntry.cs @@ -0,0 +1,4 @@ +namespace MinecraftClient +{ + public readonly record struct RecipeBookRecipeEntry(string CommandId, string DisplayText); +} diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs index 66213cc9..36687b26 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -1,1848 +1,1876 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace MinecraftClient { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class ConfigComments { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal ConfigComments() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MinecraftClient.Resources.ConfigComments.ConfigComments", typeof(ConfigComments).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to can be used in some other fields as %yourvar% - ///%username% and %serverip% are reserved variables.. - /// - internal static string AppVars_Variables { - get { - return ResourceManager.GetString("AppVars.Variables", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to =============================== # - /// Minecraft Console Client Bots # - ///=============================== #. - /// - internal static string ChatBot { - get { - return ResourceManager.GetString("ChatBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Get alerted when specified words are detected in chat - ///Useful for moderating your server or detecting when someone is talking to you. - /// - internal static string ChatBot_Alerts { - get { - return ResourceManager.GetString("ChatBot.Alerts", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Play a beep sound when a word is detected in addition to highlighting.. - /// - internal static string ChatBot_Alerts_Beep_Enabled { - get { - return ResourceManager.GetString("ChatBot.Alerts.Beep_Enabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to List of words/strings to NOT alert you on.. - /// - internal static string ChatBot_Alerts_Excludes { - get { - return ResourceManager.GetString("ChatBot.Alerts.Excludes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The name of a file where alers logs will be written.. - /// - internal static string ChatBot_Alerts_Log_File { - get { - return ResourceManager.GetString("ChatBot.Alerts.Log_File", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log alerts info a file.. - /// - internal static string ChatBot_Alerts_Log_To_File { - get { - return ResourceManager.GetString("ChatBot.Alerts.Log_To_File", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to List of words/strings to alert you on.. - /// - internal static string ChatBot_Alerts_Matches { - get { - return ResourceManager.GetString("ChatBot.Alerts.Matches", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Trigger alerts when it rains and when it stops.. - /// - internal static string ChatBot_Alerts_Trigger_By_Rain { - get { - return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Rain", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Triggers alerts at the beginning and end of thunderstorms.. - /// - internal static string ChatBot_Alerts_Trigger_By_Thunderstorm { - get { - return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Thunderstorm", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Triggers an alert after receiving a specified keyword.. - /// - internal static string ChatBot_Alerts_Trigger_By_Words { - get { - return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Words", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send a command on a regular or random basis or make the bot walk around randomly to avoid automatic AFK disconnection - /// /!\ Make sure your server rules do not forbid anti-AFK mechanisms! - /// /!\ Make sure you keep the bot in an enclosure to prevent it wandering off if you're using terrain handling! (Recommended size 5x5x5). - /// - internal static string ChatBot_AntiAfk { - get { - return ResourceManager.GetString("ChatBot.AntiAfk", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Command to send to the server.. - /// - internal static string ChatBot_AntiAfk_Command { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Command", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The time interval for execution. (in seconds). - /// - internal static string ChatBot_AntiAfk_Delay { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to sneak when sending the command.. - /// - internal static string ChatBot_AntiAfk_Use_Sneak { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Use_Sneak", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use terrain handling to enable the bot to move around.. - /// - internal static string ChatBot_AntiAfk_Use_Terrain_Handling { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Use_Terrain_Handling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The range the bot can move around randomly (Note: the bigger the range, the slower the bot will be). - /// - internal static string ChatBot_AntiAfk_Walk_Range { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Range", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How many times can the bot fail trying to move before using the command method.. - /// - internal static string ChatBot_AntiAfk_Walk_Retries { - get { - return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Retries", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically attack hostile mobs around you - ///You need to enable Entity Handling to use this bot - /// /!\ Make sure server rules allow your planned use of AutoAttack - /// /!\ SERVER PLUGINS may consider AutoAttack to be a CHEAT MOD and TAKE ACTION AGAINST YOUR ACCOUNT so DOUBLE CHECK WITH SERVER RULES!. - /// - internal static string ChatBot_AutoAttack { - get { - return ResourceManager.GetString("ChatBot.AutoAttack", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow attacking hostile mobs.. - /// - internal static string ChatBot_AutoAttack_Attack_Hostile { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Hostile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow attacking passive mobs.. - /// - internal static string ChatBot_AutoAttack_Attack_Passive { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Passive", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Capped between 1 to 4. - /// - internal static string ChatBot_AutoAttack_Attack_Range { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Range", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long to wait between each attack. Set "Custom = false" to let MCC calculate it.. - /// - internal static string ChatBot_AutoAttack_Cooldown_Time { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Cooldown_Time", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to All entity types can be found here: https://mccteam.github.io/r/entity/#L15. - /// - internal static string ChatBot_AutoAttack_Entites_List { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Entites_List", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Possible values: "Interact", "Attack" (default), "InteractAt" (Interact and Attack).. - /// - internal static string ChatBot_AutoAttack_Interaction { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Interaction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wether to treat the entities list as a "whitelist" or as a "blacklist".. - /// - internal static string ChatBot_AutoAttack_List_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.List_Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "single" or "multi". single target one mob per attack. multi target all mobs in range per attack. - /// - internal static string ChatBot_AutoAttack_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "health" or "distance". Only needed when using single mode. - /// - internal static string ChatBot_AutoAttack_Priority { - get { - return ResourceManager.GetString("ChatBot.AutoAttack.Priority", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically craft items in your inventory - ///See https://mccteam.github.io/g/bots/#auto-craft for how to use - ///You need to enable Inventory Handling to use this bot - ///You should also enable Terrain and Movements if you need to use a crafting table. - /// - internal static string ChatBot_AutoCraft { - get { - return ResourceManager.GetString("ChatBot.AutoCraft", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Location of the crafting table if you intended to use it. Terrain and movements must be enabled.. - /// - internal static string ChatBot_AutoCraft_CraftingTable { - get { - return ResourceManager.GetString("ChatBot.AutoCraft.CraftingTable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to What to do on crafting failure, "abort" or "wait".. - /// - internal static string ChatBot_AutoCraft_OnFailure { - get { - return ResourceManager.GetString("ChatBot.AutoCraft.OnFailure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Recipes.Name: The name can be whatever you like and it is used to represent the recipe. - ///Recipes.Type: crafting table type: "player" or "table" - ///Recipes.Result: the resulting item - ///Recipes.Slots: All slots, counting from left to right, top to bottom. Please fill in "Null" for empty slots. - ///For the naming of the items, please see: https://mccteam.github.io/r/item/#L12. - /// - internal static string ChatBot_AutoCraft_Recipes { - get { - return ResourceManager.GetString("ChatBot.AutoCraft.Recipes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Auto-digging blocks. - ///You need to enable Terrain Handling to use this bot - ///You can use "/digbot start" and "/digbot stop" to control the start and stop of AutoDig. - ///Since MCC does not yet support accurate calculation of the collision volume of blocks, all blocks are considered as complete cubes when obtaining the position of the lookahead. - ///For the naming of the block, please see https://mccteam.github.io/r/block/#L15. - /// - internal static string ChatBot_AutoDig { - get { - return ResourceManager.GetString("ChatBot.AutoDig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How many seconds to wait after entering the game to start digging automatically, set to -1 to disable automatic start.. - /// - internal static string ChatBot_AutoDig_Auto_Start_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Auto_Start_Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically switch to the appropriate tool.. - /// - internal static string ChatBot_AutoDig_Auto_Tool_Switch { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Auto_Tool_Switch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Mining a block for more than "Dig_Timeout" seconds will be considered a timeout.. - /// - internal static string ChatBot_AutoDig_Dig_Timeout { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Dig_Timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to drop the current tool when its durability is too low.. - /// - internal static string ChatBot_AutoDig_Drop_Low_Durability_Tools { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Drop_Low_Durability_Tools", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Will not use tools with less durability than this. Set to zero to disable this feature.. - /// - internal static string ChatBot_AutoDig_Durability_Limit { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Durability_Limit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wether to treat the blocks list as a "whitelist" or as a "blacklist".. - /// - internal static string ChatBot_AutoDig_List_Type { - get { - return ResourceManager.GetString("ChatBot.AutoDig.List_Type", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "distance" or "index", When using the "fixedpos" mode, the blocks are determined by distance to the player, or by the order in the list.. - /// - internal static string ChatBot_AutoDig_Location_Order { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Location_Order", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The position of the blocks when using "fixedpos" or "both" mode.. - /// - internal static string ChatBot_AutoDig_Locations { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Locations", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to output logs when digging blocks.. - /// - internal static string ChatBot_AutoDig_Log_Block_Dig { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Log_Block_Dig", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "lookat", "fixedpos" or "both". Digging the block being looked at, the block in a fixed position, or the block that needs to be all met.. - /// - internal static string ChatBot_AutoDig_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoDig.Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically drop items in inventory - ///You need to enable Inventory Handling to use this bot - ///See this file for an up-to-date list of item types you can use with this bot: https://mccteam.github.io/r/item/#L12. - /// - internal static string ChatBot_AutoDrop { - get { - return ResourceManager.GetString("ChatBot.AutoDrop", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "include", "exclude" or "everything". Include: drop item IN the list. Exclude: drop item NOT IN the list. - /// - internal static string ChatBot_AutoDrop_Mode { - get { - return ResourceManager.GetString("ChatBot.AutoDrop.Mode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically eat food when your Hunger value is low - ///You need to enable Inventory Handling to use this bot. - /// - internal static string ChatBot_AutoEat { - get { - return ResourceManager.GetString("ChatBot.AutoEat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically catch fish using a fishing rod - ///Guide: https://mccteam.github.io/g/bots/#auto-fishing - ///You can use "/fish" to control the bot manually. - /// /!\ Make sure server rules allow automated farming before using this bot. - /// - internal static string ChatBot_AutoFishing { - get { - return ResourceManager.GetString("ChatBot.AutoFishing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Keep it as false if you have not changed it before.. - /// - internal static string ChatBot_AutoFishing_Antidespawn { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Antidespawn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Switch to a new rod from inventory after the current rod is unavailable.. - /// - internal static string ChatBot_AutoFishing_Auto_Rod_Switch { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Rod_Switch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to start fishing automatically after entering a world.. - /// - internal static string ChatBot_AutoFishing_Auto_Start { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Start", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How soon to re-cast after successful fishing.. - /// - internal static string ChatBot_AutoFishing_Cast_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Cast_Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Will not use rods with less durability than this (full durability is 64). Set to zero to disable this feature.. - /// - internal static string ChatBot_AutoFishing_Durability_Limit { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Durability_Limit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This allows the player to change position/facing after each fish caught.. - /// - internal static string ChatBot_AutoFishing_Enable_Move { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Enable_Move", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long after entering the game to start fishing (seconds).. - /// - internal static string ChatBot_AutoFishing_Fishing_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fishing timeout (seconds). Timeout will trigger a re-cast.. - /// - internal static string ChatBot_AutoFishing_Fishing_Timeout { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.. - /// - internal static string ChatBot_AutoFishing_Hook_Threshold { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Hook_Threshold", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet.. - /// - internal static string ChatBot_AutoFishing_Log_Fish_Bobber { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Log_Fish_Bobber", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use the mainhand or the offhand to hold the rod.. - /// - internal static string ChatBot_AutoFishing_Mainhand { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Mainhand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to It will move in order "1->2->3->4->3->2->1->2->..." and can change position or facing or both each time. It is recommended to change the facing only.. - /// - internal static string ChatBot_AutoFishing_Movements { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Movements", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Hook movement in the X and Z axis less than this value will be considered stationary.. - /// - internal static string ChatBot_AutoFishing_Stationary_Threshold { - get { - return ResourceManager.GetString("ChatBot.AutoFishing.Stationary_Threshold", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically relog when disconnected by server, for example because the server is restating - /// /!\ Use Ignore_Kick_Message=true at own risk! Server staff might not appreciate if you auto-relog on manual kicks. - /// - internal static string ChatBot_AutoRelog { - get { - return ResourceManager.GetString("ChatBot.AutoRelog", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The delay time before joining the server. (in seconds). - /// - internal static string ChatBot_AutoRelog_Delay { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When set to true, autorelog will reconnect regardless of kick messages.. - /// - internal static string ChatBot_AutoRelog_Ignore_Kick_Message { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Ignore_Kick_Message", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If the kickout message matches any of the strings, then autorelog will be triggered.. - /// - internal static string ChatBot_AutoRelog_Kick_Messages { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Kick_Messages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Retries when failing to relog to the server. use -1 for unlimited retries.. - /// - internal static string ChatBot_AutoRelog_Retries { - get { - return ResourceManager.GetString("ChatBot.AutoRelog.Retries", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Run commands or send messages automatically when a specified pattern is detected in chat - ///Server admins can spoof chat messages (/nick, /tellraw) so keep this in mind when implementing AutoRespond rules - /// /!\ This bot may get spammy depending on your rules, although the global messagecooldown setting can help you avoiding accidental spam. - /// - internal static string ChatBot_AutoRespond { - get { - return ResourceManager.GetString("ChatBot.AutoRespond", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not remove colors from text (Note: Your matches will have to include color codes (ones using the § character) in order to work). - /// - internal static string ChatBot_AutoRespond_Match_Colors { - get { - return ResourceManager.GetString("ChatBot.AutoRespond.Match_Colors", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Logs chat messages in a file on disk.. - /// - internal static string ChatBot_ChatLog { - get { - return ResourceManager.GetString("ChatBot.ChatLog", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This bot allows you to send and recieve messages and commands via a Discord channel. - ///For Setup you can either use the documentation or read here (Documentation has images). - ///Documentation: https://mccteam.github.io/g/bots/#discord-bridge - ///Setup: - ///First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA . - /// /!\ IMPORTANT /!\: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent [rest of string was truncated]";. - /// - internal static string ChatBot_DiscordBridge { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The ID of a channel where you want to interact with the MCC using the bot.. - /// - internal static string ChatBot_DiscordBridge_ChannelId { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.ChannelId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Message formats - ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! - ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. - ///For Discord message formatting, check the following: https://mccteam.github.io/r/dc-fmt.html. - /// - internal static string ChatBot_DiscordBridge_Formats { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.Formats", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The ID of a server/guild where you have invited the bot to.. - /// - internal static string ChatBot_DiscordBridge_GuildId { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.GuildId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to discord before canceling the task (minimum 1 second).. - /// - internal static string ChatBot_DiscordBridge_MessageSendTimeout { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.MessageSendTimeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A list of IDs of people you want to be able to interact with the MCC using the bot.. - /// - internal static string ChatBot_DiscordBridge_OwnersIds { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.OwnersIds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Your Discord Bot token.. - /// - internal static string ChatBot_DiscordBridge_Token { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.Token", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat.. - /// - internal static string ChatBot_DiscordBridge_AllowOtherBotMessages { - get { - return ResourceManager.GetString("ChatBot.DiscordBridge.AllowOtherBotMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically farms cropsfor you (plants, breaks and bonemeals them). - ///Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat. - ///Usage: "/farmer start" command and "/farmer stop" command. - ///NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes. - ///or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this. [rest of string was truncated]";. - /// - internal static string ChatBot_Farmer { - get { - return ResourceManager.GetString("ChatBot.Farmer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delay between tasks in seconds (Minimum 1 second). - /// - internal static string ChatBot_Farmer_Delay_Between_Tasks { - get { - return ResourceManager.GetString("ChatBot.Farmer.Delay_Between_Tasks", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enabled you to make the bot follow you - ///NOTE: This is an experimental feature, the bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you - ///It's similar to making animals follow you when you're holding food in your hand. - ///This is due to a slow pathfinding algorithm, we're working on getting a better one - ///You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite, /// [rest of string was truncated]";. - /// - internal static string ChatBot_FollowPlayer { - get { - return ResourceManager.GetString("ChatBot.FollowPlayer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Do not follow the player if he is in the range of 3 blocks (prevents the bot from pushing a player in an infinite loop). - /// - internal static string ChatBot_FollowPlayer_Stop_At_Distance { - get { - return ResourceManager.GetString("ChatBot.FollowPlayer.Stop_At_Distance", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The rate at which the bot does calculations (in seconds) (You can tweak this if you feel the bot is too slow). - /// - internal static string ChatBot_FollowPlayer_Update_Limit { - get { - return ResourceManager.GetString("ChatBot.FollowPlayer.Update_Limit", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A small game to demonstrate chat interactions. Players can guess mystery words one letter at a time. - ///You need to have ChatFormat working correctly and add yourself in botowners to start the game with /tell <bot username> start - /// /!\ This bot may get a bit spammy if many players are interacting with it. - /// - internal static string ChatBot_HangmanGame { - get { - return ResourceManager.GetString("ChatBot.HangmanGame", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A Chat Bot that collects items on the ground. - /// - internal static string ChatBot_ItemsCollector { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If set to true, the bot will return to it's starting position after there are no items to collect. - /// - internal static string ChatBot_ItemsCollector_Always_Return_To_Start { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Always_Return_To_Start", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If set to true, the bot will collect all items, regardless of their type. If you want to use the whitelisted item types, disable this by setting it to false. - /// - internal static string ChatBot_ItemsCollector_Collect_All_Item_Types { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Collect_All_Item_Types", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The radius in which bot will look for items to collect (Default: 30). - /// - internal static string ChatBot_ItemsCollector_Collection_Radius { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Collection_Radius", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delay in milliseconds between bot scanning items (Recommended: 300-500). - /// - internal static string ChatBot_ItemsCollector_Delay_Between_Tasks { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Delay_Between_Tasks", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to In this list you can specify which items the bot will collect. To enable this, set the Collect_All_Item_Types to false. (NOTE: This does not prevent the bot from accidentally picking up other items, it only goes to positions where it finds the whitelisted items)\nYou can see the list of item types here: https://raw.githubusercontent.com/MCCTeam/Minecraft-Console-Client/master/MinecraftClient/Inventory/ItemType.cs. - /// - internal static string ChatBot_ItemsCollector_Items_Whitelist { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Items_Whitelist", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If set to true, the bot will go after clustered items instead for the closest ones. - /// - internal static string ChatBot_ItemsCollector_Prioritize_Clusters { - get { - return ResourceManager.GetString("ChatBot.ItemsCollector.Prioritize_Clusters", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info. - ///Setup: - ///1. Go to https://discord.com/developers/applications and log in with your Discord account. [rest of string was truncated]";. - /// - internal static string ChatBot_DiscordRpc { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Your Discord Application ID.. - /// - internal static string ChatBot_DiscordRpc_ApplicationId { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ApplicationId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The top line of the Rich Presence display. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_PresenceDetails { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceDetails", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The second line of the Rich Presence display. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_PresenceState { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceState", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The key of the large image asset uploaded to your Discord application.. - /// - internal static string ChatBot_DiscordRpc_LargeImageKey { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tooltip text for the large image. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_LargeImageText { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The key of the small image asset uploaded to your Discord application (leave empty to hide).. - /// - internal static string ChatBot_DiscordRpc_SmallImageKey { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageKey", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tooltip text for the small image. Supports placeholders.. - /// - internal static string ChatBot_DiscordRpc_SmallImageText { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageText", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the server address (host and port) in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowServerAddress { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowServerAddress", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the player coordinates in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowCoordinates { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowCoordinates", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show health and food level in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowHealth { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowHealth", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the current dimension in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowDimension { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowDimension", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the current gamemode in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowGamemode { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowGamemode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show elapsed session time in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowElapsedTime { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowElapsedTime", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show the online player count as a party size in the Discord presence.. - /// - internal static string ChatBot_DiscordRpc_ShowPlayerCount { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.ShowPlayerCount", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How often (in seconds) to refresh the Discord presence. Minimum: 1. - /// - internal static string ChatBot_DiscordRpc_UpdateIntervalSeconds { - get { - return ResourceManager.GetString("ChatBot.DiscordRpc.UpdateIntervalSeconds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Relay messages between players and servers, like a mail plugin - ///This bot can store messages when the recipients are offline, and send them when they join the server - /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable this bot only if you trust server admins. - /// - internal static string ChatBot_Mailer { - get { - return ResourceManager.GetString("ChatBot.Mailer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allows you to render maps in the console and into images (which can be then sent to Discord using Discord Bridge Chat Bot) - ///This is useful for solving captchas which use maps - ///The maps are rendered into Rendered_Maps folder if the Save_To_File is enabled. - ///NOTE: - ///If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update to see them immediatelly in the console. - /// /!\ Make sure server rules allow bots to be used on the server, or you risk being punished.. - /// - internal static string ChatBot_Map { - get { - return ResourceManager.GetString("ChatBot.Map", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Automatically render the map once it is received or updated from/by the server. - /// - internal static string ChatBot_Map_Auto_Render_On_Update { - get { - return ResourceManager.GetString("ChatBot.Map.Auto_Render_On_Update", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Delete all rendered maps on unload/reload or when you launch the MCC again.. - /// - internal static string ChatBot_Map_Delete_All_On_Unload { - get { - return ResourceManager.GetString("ChatBot.Map.Delete_All_On_Unload", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Get a notification when you have gotten a map from the server for the first time. - /// - internal static string ChatBot_Map_Notify_On_First_Update { - get { - return ResourceManager.GetString("ChatBot.Map.Notify_On_First_Update", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord.. - /// - internal static string ChatBot_Map_Rasize_Rendered_Image { - get { - return ResourceManager.GetString("ChatBot.Map.Rasize_Rendered_Image", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to render the map in the console.. - /// - internal static string ChatBot_Map_Render_In_Console { - get { - return ResourceManager.GetString("ChatBot.Map.Render_In_Console", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The size that a rendered image should be resized to, in pixels (eg. 512).. - /// - internal static string ChatBot_Map_Resize_To { - get { - return ResourceManager.GetString("ChatBot.Map.Resize_To", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to store the rendered map as a file (You need this setting if you want to get a map on Discord using Discord Bridge).. - /// - internal static string ChatBot_Map_Save_To_File { - get { - return ResourceManager.GetString("ChatBot.Map.Save_To_File", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send a rendered map (saved to a file) to a Discord or a Telegram channel via the Discord or Telegram Bride chat bot (The Discord/Telegram Bridge chat bot must be enabled and configured!) - ///You need to enable Save_To_File in order for this to work. - ///We also recommend turning on resizing.. - /// - internal static string ChatBot_Map_Send_Rendered_To_Bridges { - get { - return ResourceManager.GetString("ChatBot.Map.Send_Rendered_To_Bridges", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log the list of players periodically into a textual file.. - /// - internal static string ChatBot_PlayerListLogger { - get { - return ResourceManager.GetString("ChatBot.PlayerListLogger", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to (In seconds). - /// - internal static string ChatBot_PlayerListLogger_Delay { - get { - return ResourceManager.GetString("ChatBot.PlayerListLogger.Delay", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Send MCC console commands to your bot through server PMs (/tell) - ///You need to have ChatFormat working correctly and add yourself in botowners to use the bot - /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable RemoteControl only if you trust server admins. - /// - internal static string ChatBot_RemoteControl { - get { - return ResourceManager.GetString("ChatBot.RemoteControl", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable recording of the game (/replay start) and replay it later using the Replay Mod (https://www.replaymod.com/) - ///Please note that due to technical limitations, the client player (you) will not be shown in the replay file - /// /!\ You SHOULD use /replay stop or exit the program gracefully with /quit OR THE REPLAY FILE MAY GET CORRUPT!. - /// - internal static string ChatBot_ReplayCapture { - get { - return ResourceManager.GetString("ChatBot.ReplayCapture", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long should replay file be auto-saved, in seconds. Use -1 to disable.. - /// - internal static string ChatBot_ReplayCapture_Backup_Interval { - get { - return ResourceManager.GetString("ChatBot.ReplayCapture.Backup_Interval", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Schedule commands and scripts to launch on various events such as server join, date/time or time interval - ///See https://mccteam.github.io/g/bots/#script-scheduler for more info. - /// - internal static string ChatBot_ScriptScheduler { - get { - return ResourceManager.GetString("ChatBot.ScriptScheduler", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel. - /// /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel. - ///----------------------------------------------------------- - ///Setup: - ///First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather - ///Click on "Start" button and re [rest of string was truncated]";. - /// - internal static string ChatBot_TelegramBridge { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A list of Chat IDs that are allowed to send messages and execute commands. To get an id of your chat DM with the bot use ".chatid" bot command in Telegram.. - /// - internal static string ChatBot_TelegramBridge_Authorized_Chat_Ids { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.Authorized_Chat_Ids", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to An ID of a channel where you want to interact with the MCC using the bot.. - /// - internal static string ChatBot_TelegramBridge_ChannelId { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.ChannelId", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Message formats - ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! - ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. - ///For Telegram message formatting, check the following: https://mccteam.github.io/r/tg-fmt.html. - /// - internal static string ChatBot_TelegramBridge_Formats { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.Formats", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second).. - /// - internal static string ChatBot_TelegramBridge_MessageSendTimeout { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.MessageSendTimeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Your Telegram Bot token.. - /// - internal static string ChatBot_TelegramBridge_Token { - get { - return ResourceManager.GetString("ChatBot.TelegramBridge.Token", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Remotely control the client using Web Sockets.\n# This is useful if you want to implement an application that can remotely and asynchronously execute procedures in MCC.\n# Example implementation written in JavaScript: https://github.com/milutinke/MCC.js.git\n# The protocol specification will be available in the documentation soon.. - /// - internal static string ChatBot_WebSocketBot { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allow IP aliases, such as "localhost" or if using containers then the container name can be used.... - /// - internal static string ChatBot_WebSocketBot_AllowIpAlias { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.AllowIpAlias", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This setting is for developers who are developing a library that uses this chat bot to remotely execute procedures/commands/functions.. - /// - internal static string ChatBot_WebSocketBot_DebugMode { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.DebugMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The IP address that Websocket server will be bound to.. - /// - internal static string ChatBot_WebSocketBot_Ip { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.Ip", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A password that will be used to authenticate on thw Websocket server (It is recommended to change the default password and to set a strong one).. - /// - internal static string ChatBot_WebSocketBot_Password { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The Port that Websocket server will be bounded to.. - /// - internal static string ChatBot_WebSocketBot_Port { - get { - return ResourceManager.GetString("ChatBot.WebSocketBot.Port", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MCC does it best to detect chat messages, but some server have unusual chat formats - ///When this happens, you'll need to configure chat format below, see https://mccteam.github.io/g/conf/#chat-format-section. - /// - internal static string ChatFormat { - get { - return ResourceManager.GetString("ChatFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MCC support for common message formats. Set "false" to avoid conflicts with custom formats.. - /// - internal static string ChatFormat_Builtins { - get { - return ResourceManager.GetString("ChatFormat.Builtins", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to use the custom regular expressions below for detection.. - /// - internal static string ChatFormat_UserDefined { - get { - return ResourceManager.GetString("ChatFormat.UserDefined", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Console-related settings.. - /// - internal static string Console { - get { - return ResourceManager.GetString("Console", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The settings for command completion suggestions. - ///Custom colors are only available when using "vt100_24bit" color mode.. - /// - internal static string Console_CommandSuggestion { - get { - return ResourceManager.GetString("Console.CommandSuggestion", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to display command suggestions in the console.. - /// - internal static string Console_CommandSuggestion_Enable { - get { - return ResourceManager.GetString("Console.CommandSuggestion.Enable", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable this option if the arrows in the command suggestions are not displayed properly in your terminal.. - /// - internal static string Console_CommandSuggestion_Use_Basic_Arrow { - get { - return ResourceManager.GetString("Console.CommandSuggestion.Use_Basic_Arrow", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Console mode: "classic" for the standard terminal, "tui" for a pseudo-graphical full-screen interface.. - /// - internal static string Console_General_ConsoleMode { - get { - return ResourceManager.GetString("Console.General.ConsoleMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "disable", "legacy_4bit", "vt100_4bit", "vt100_8bit" or "vt100_24bit". If a garbled code like "←[0m" appears on the terminal, you can try switching to "legacy_4bit" mode, or just disable it.. - /// - internal static string Console_General_ConsoleColorMode { - get { - return ResourceManager.GetString("Console.General.ConsoleColorMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to You can use "Ctrl+P" to print out the current input and cursor position.. - /// - internal static string Console_General_Display_Input { - get { - return ResourceManager.GetString("Console.General.Display_Input", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Startup Config File - ///Please do not record extraneous data in this file as it will be overwritten by MCC. - /// - ///New to Minecraft Console Client? Check out this document: https://mccteam.github.io/g/conf.html - ///Want to upgrade to a newer version? See https://github.com/MCCTeam/Minecraft-Console-Client/#download. - /// - internal static string Head { - get { - return ResourceManager.GetString("Head", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to This setting affects only the messages in the console.. - /// - internal static string Logging { - get { - return ResourceManager.GetString("Logging", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regex for filtering chat message.. - /// - internal static string Logging_ChatFilter { - get { - return ResourceManager.GetString("Logging.ChatFilter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show server chat messages.. - /// - internal static string Logging_ChatMessages { - get { - return ResourceManager.GetString("Logging.ChatMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Regex for filtering debug message.. - /// - internal static string Logging_DebugFilter { - get { - return ResourceManager.GetString("Logging.DebugFilter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please enable this before submitting bug reports. Thanks!. - /// - internal static string Logging_DebugMessages { - get { - return ResourceManager.GetString("Logging.DebugMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show error messages.. - /// - internal static string Logging_ErrorMessages { - get { - return ResourceManager.GetString("Logging.ErrorMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to "disable" or "blacklist" OR "whitelist". Blacklist hide message match regex. Whitelist show message match regex.. - /// - internal static string Logging_FilterMode { - get { - return ResourceManager.GetString("Logging.FilterMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Informative messages. (i.e Most of the message from MCC). - /// - internal static string Logging_InfoMessages { - get { - return ResourceManager.GetString("Logging.InfoMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Log file name.. - /// - internal static string Logging_LogFile { - get { - return ResourceManager.GetString("Logging.LogFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Write log messages to file.. - /// - internal static string Logging_LogToFile { - get { - return ResourceManager.GetString("Logging.LogToFile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prepend timestamp to messages in log file.. - /// - internal static string Logging_PrependTimestamp { - get { - return ResourceManager.GetString("Logging.PrependTimestamp", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Keep color codes in the saved text.(look like "§b"). - /// - internal static string Logging_SaveColorCodes { - get { - return ResourceManager.GetString("Logging.SaveColorCodes", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show warning messages.. - /// - internal static string Logging_WarningMessages { - get { - return ResourceManager.GetString("Logging.WarningMessages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Make sure you understand what each setting does before changing anything!. - /// - internal static string Main_Advanced { - get { - return ResourceManager.GetString("Main.Advanced", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AccountList: It allows a fast account switching without directly using the credentials - ///Usage examples: "/tell <mybot> reco Player2", "/connect <serverip> Player1". - /// - internal static string Main_Advanced_account_list { - get { - return ResourceManager.GetString("Main.Advanced.account_list", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle auto respawn if client player was dead (make sure your spawn point is safe).. - /// - internal static string Main_Advanced_auto_respawn { - get { - return ResourceManager.GetString("Main.Advanced.auto_respawn", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set the owner of the bot. /!\ Server admins can impersonate owners!. - /// - internal static string Main_Advanced_bot_owners { - get { - return ResourceManager.GetString("Main.Advanced.bot_owners", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "mcc", "vanilla" or "none". This is how MCC identifies itself to the server.. - /// - internal static string Main_Advanced_brand_info { - get { - return ResourceManager.GetString("Main.Advanced.brand_info", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Leave empty for no logfile.. - /// - internal static string Main_Advanced_chatbot_log_file { - get { - return ResourceManager.GetString("Main.Advanced.chatbot_log_file", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If turned off, the emoji will be replaced with a simpler character (for /chunk status).. - /// - internal static string Main_Advanced_enable_emoji { - get { - return ResourceManager.GetString("Main.Advanced.enable_emoji", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set to false to opt-out of Sentry error logging.. - /// - internal static string Main_Advanced_enable_sentry { - get { - return ResourceManager.GetString("Main.Advanced.enable_sentry", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle entity handling.. - /// - internal static string Main_Advanced_entity_handling { - get { - return ResourceManager.GetString("Main.Advanced.entity_handling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to exit directly when an error occurs, for using MCC in non-interactive scripts.. - /// - internal static string Main_Advanced_exit_on_failure { - get { - return ResourceManager.GetString("Main.Advanced.exit_on_failure", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ignore invalid player name. - /// - internal static string Main_Advanced_ignore_invalid_playername { - get { - return ResourceManager.GetString("Main.Advanced.ignore_invalid_playername", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "none", "slash"(/) or "backslash"(\).. - /// - internal static string Main_Advanced_internal_cmd_char { - get { - return ResourceManager.GetString("Main.Advanced.internal_cmd_char", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Toggle inventory handling.. - /// - internal static string Main_Advanced_inventory_handling { - get { - return ResourceManager.GetString("Main.Advanced.inventory_handling", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fill in with in-game locale code, check https://mccteam.github.io/r/l-code.html. - /// - internal static string Main_Advanced_language { - get { - return ResourceManager.GetString("Main.Advanced.language", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Load translations applied to MCC when available, turn it off to use English only.. - /// - internal static string Main_Advanced_LoadMccTrans { - get { - return ResourceManager.GetString("Main.Advanced.LoadMccTrans", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "auto", "no" or "force". Force-enabling only works for MC 1.13+.. - /// - internal static string Main_Advanced_mc_forge { - get { - return ResourceManager.GetString("Main.Advanced.mc_forge", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "auto" or "1.X.X" values. Allows to skip server info retrieval.. - /// - internal static string Main_Advanced_mc_version { - get { - return ResourceManager.GetString("Main.Advanced.mc_version", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Controls the minimum interval (in seconds) between sending each message to the server.. - /// - internal static string Main_Advanced_message_cooldown { - get { - return ResourceManager.GetString("Main.Advanced.message_cooldown", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Override the maximum chat message length. Set to 0 to use the default (100 for 1.10 and below, 256 for 1.11+). WARNING: Setting this incorrectly may cause you to be kicked from the server.. - /// - internal static string Main_Advanced_max_chat_message_length { - get { - return ResourceManager.GetString("Main.Advanced.max_chat_message_length", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable support for joining Minecraft Realms worlds.. - /// - internal static string Main_Advanced_minecraft_realms { - get { - return ResourceManager.GetString("Main.Advanced.minecraft_realms", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The minimum height to use when calculating the image size from the height of the terminal.. - /// - internal static string Main_Advanced_MinTerminalHeight { - get { - return ResourceManager.GetString("Main.Advanced.MinTerminalHeight", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The minimum width used when calculating the image size from the width of the terminal.. - /// - internal static string Main_Advanced_MinTerminalWidth { - get { - return ResourceManager.GetString("Main.Advanced.MinTerminalWidth", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Enable head movement while walking to avoid anti-cheat triggers.. - /// - internal static string Main_Advanced_move_head_while_walking { - get { - return ResourceManager.GetString("Main.Advanced.move_head_while_walking", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A movement speed higher than 2 may be considered cheating.. - /// - internal static string Main_Advanced_movement_speed { - get { - return ResourceManager.GetString("Main.Advanced.movement_speed", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only works on Windows XP-8 or Windows 10 with old console.. - /// - internal static string Main_Advanced_player_head_icon { - get { - return ResourceManager.GetString("Main.Advanced.player_head_icon", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to For remote control of the bot.. - /// - internal static string Main_Advanced_private_msgs_cmd_name { - get { - return ResourceManager.GetString("Main.Advanced.private_msgs_cmd_name", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How to retain profile key. Use "none", "memory" or "disk".. - /// - internal static string Main_Advanced_profilekey_cache { - get { - return ResourceManager.GetString("Main.Advanced.profilekey_cache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "no", "fast" (5s timeout), or "yes". Required for joining some servers.. - /// - internal static string Main_Advanced_resolve_srv_records { - get { - return ResourceManager.GetString("Main.Advanced.resolve_srv_records", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cache compiled scripts for faster load on low-end devices.. - /// - internal static string Main_Advanced_script_cache { - get { - return ResourceManager.GetString("Main.Advanced.script_cache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ServerList: It allows an easier and faster server switching with short aliases instead of full server IP - ///Aliases cannot contain dots or spaces, and the name "localhost" cannot be used as an alias. - ///Usage examples: "/tell <mybot> connect Server1", "/connect Server2". - /// - internal static string Main_Advanced_server_list { - get { - return ResourceManager.GetString("Main.Advanced.server_list", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to How to retain session tokens. Use "none", "memory" or "disk".. - /// - internal static string Main_Advanced_session_cache { - get { - return ResourceManager.GetString("Main.Advanced.session_cache", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Decode links embedded in chat messages and show them in console.. - /// - internal static string Main_Advanced_show_chat_links { - get { - return ResourceManager.GetString("Main.Advanced.show_chat_links", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Show inventory layout as ASCII art in inventory command.. - /// +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MinecraftClient { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ConfigComments { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ConfigComments() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MinecraftClient.Resources.ConfigComments.ConfigComments", typeof(ConfigComments).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to can be used in some other fields as %yourvar% + ///%username% and %serverip% are reserved variables.. + /// + internal static string AppVars_Variables { + get { + return ResourceManager.GetString("AppVars.Variables", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to =============================== # + /// Minecraft Console Client Bots # + ///=============================== #. + /// + internal static string ChatBot { + get { + return ResourceManager.GetString("ChatBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get alerted when specified words are detected in chat + ///Useful for moderating your server or detecting when someone is talking to you. + /// + internal static string ChatBot_Alerts { + get { + return ResourceManager.GetString("ChatBot.Alerts", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Play a beep sound when a word is detected in addition to highlighting.. + /// + internal static string ChatBot_Alerts_Beep_Enabled { + get { + return ResourceManager.GetString("ChatBot.Alerts.Beep_Enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List of words/strings to NOT alert you on.. + /// + internal static string ChatBot_Alerts_Excludes { + get { + return ResourceManager.GetString("ChatBot.Alerts.Excludes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of a file where alers logs will be written.. + /// + internal static string ChatBot_Alerts_Log_File { + get { + return ResourceManager.GetString("ChatBot.Alerts.Log_File", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log alerts info a file.. + /// + internal static string ChatBot_Alerts_Log_To_File { + get { + return ResourceManager.GetString("ChatBot.Alerts.Log_To_File", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List of words/strings to alert you on.. + /// + internal static string ChatBot_Alerts_Matches { + get { + return ResourceManager.GetString("ChatBot.Alerts.Matches", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Trigger alerts when it rains and when it stops.. + /// + internal static string ChatBot_Alerts_Trigger_By_Rain { + get { + return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Rain", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Triggers alerts at the beginning and end of thunderstorms.. + /// + internal static string ChatBot_Alerts_Trigger_By_Thunderstorm { + get { + return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Thunderstorm", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Triggers an alert after receiving a specified keyword.. + /// + internal static string ChatBot_Alerts_Trigger_By_Words { + get { + return ResourceManager.GetString("ChatBot.Alerts.Trigger_By_Words", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send a command on a regular or random basis or make the bot walk around randomly to avoid automatic AFK disconnection + /// /!\ Make sure your server rules do not forbid anti-AFK mechanisms! + /// /!\ Make sure you keep the bot in an enclosure to prevent it wandering off if you're using terrain handling! (Recommended size 5x5x5). + /// + internal static string ChatBot_AntiAfk { + get { + return ResourceManager.GetString("ChatBot.AntiAfk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Command to send to the server.. + /// + internal static string ChatBot_AntiAfk_Command { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Command", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The time interval for execution. (in seconds). + /// + internal static string ChatBot_AntiAfk_Delay { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to sneak when sending the command.. + /// + internal static string ChatBot_AntiAfk_Use_Sneak { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Use_Sneak", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use terrain handling to enable the bot to move around.. + /// + internal static string ChatBot_AntiAfk_Use_Terrain_Handling { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Use_Terrain_Handling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The range the bot can move around randomly (Note: the bigger the range, the slower the bot will be). + /// + internal static string ChatBot_AntiAfk_Walk_Range { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Range", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How many times can the bot fail trying to move before using the command method.. + /// + internal static string ChatBot_AntiAfk_Walk_Retries { + get { + return ResourceManager.GetString("ChatBot.AntiAfk.Walk_Retries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically attack hostile mobs around you + ///You need to enable Entity Handling to use this bot + /// /!\ Make sure server rules allow your planned use of AutoAttack + /// /!\ SERVER PLUGINS may consider AutoAttack to be a CHEAT MOD and TAKE ACTION AGAINST YOUR ACCOUNT so DOUBLE CHECK WITH SERVER RULES!. + /// + internal static string ChatBot_AutoAttack { + get { + return ResourceManager.GetString("ChatBot.AutoAttack", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow attacking hostile mobs.. + /// + internal static string ChatBot_AutoAttack_Attack_Hostile { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Hostile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow attacking passive mobs.. + /// + internal static string ChatBot_AutoAttack_Attack_Passive { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Passive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Capped between 1 to 4. + /// + internal static string ChatBot_AutoAttack_Attack_Range { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Attack_Range", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long to wait between each attack. Set "Custom = false" to let MCC calculate it.. + /// + internal static string ChatBot_AutoAttack_Cooldown_Time { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Cooldown_Time", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entity types can be found here: https://mccteam.github.io/r/entity/#L15. + /// + internal static string ChatBot_AutoAttack_Entites_List { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Entites_List", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Possible values: "Interact", "Attack" (default), "InteractAt" (Interact and Attack).. + /// + internal static string ChatBot_AutoAttack_Interaction { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Interaction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wether to treat the entities list as a "whitelist" or as a "blacklist".. + /// + internal static string ChatBot_AutoAttack_List_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.List_Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "single" or "multi". single target one mob per attack. multi target all mobs in range per attack. + /// + internal static string ChatBot_AutoAttack_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "health" or "distance". Only needed when using single mode. + /// + internal static string ChatBot_AutoAttack_Priority { + get { + return ResourceManager.GetString("ChatBot.AutoAttack.Priority", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically craft items in your inventory + ///See https://mccteam.github.io/g/bots/#auto-craft for how to use + ///You need to enable Inventory Handling to use this bot + ///You should also enable Terrain and Movements if you need to use a crafting table. + /// + internal static string ChatBot_AutoCraft { + get { + return ResourceManager.GetString("ChatBot.AutoCraft", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location of the crafting table if you intended to use it. Terrain and movements must be enabled.. + /// + internal static string ChatBot_AutoCraft_CraftingTable { + get { + return ResourceManager.GetString("ChatBot.AutoCraft.CraftingTable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to What to do on crafting failure, "abort" or "wait".. + /// + internal static string ChatBot_AutoCraft_OnFailure { + get { + return ResourceManager.GetString("ChatBot.AutoCraft.OnFailure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recipes.Name: The name can be whatever you like and it is used to represent the recipe. + ///Recipes.Type: crafting table type: "player" or "table" + ///Recipes.Result: the resulting item + ///Recipes.Slots: All slots, counting from left to right, top to bottom. Please fill in "Null" for empty slots. + ///For the naming of the items, please see: https://mccteam.github.io/r/item/#L12. + /// + internal static string ChatBot_AutoCraft_Recipes { + get { + return ResourceManager.GetString("ChatBot.AutoCraft.Recipes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Auto-digging blocks. + ///You need to enable Terrain Handling to use this bot + ///You can use "/digbot start" and "/digbot stop" to control the start and stop of AutoDig. + ///Since MCC does not yet support accurate calculation of the collision volume of blocks, all blocks are considered as complete cubes when obtaining the position of the lookahead. + ///For the naming of the block, please see https://mccteam.github.io/r/block/#L15. + /// + internal static string ChatBot_AutoDig { + get { + return ResourceManager.GetString("ChatBot.AutoDig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How many seconds to wait after entering the game to start digging automatically, set to -1 to disable automatic start.. + /// + internal static string ChatBot_AutoDig_Auto_Start_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Auto_Start_Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically switch to the appropriate tool.. + /// + internal static string ChatBot_AutoDig_Auto_Tool_Switch { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Auto_Tool_Switch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mining a block for more than "Dig_Timeout" seconds will be considered a timeout.. + /// + internal static string ChatBot_AutoDig_Dig_Timeout { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Dig_Timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to drop the current tool when its durability is too low.. + /// + internal static string ChatBot_AutoDig_Drop_Low_Durability_Tools { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Drop_Low_Durability_Tools", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Will not use tools with less durability than this. Set to zero to disable this feature.. + /// + internal static string ChatBot_AutoDig_Durability_Limit { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Durability_Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wether to treat the blocks list as a "whitelist" or as a "blacklist".. + /// + internal static string ChatBot_AutoDig_List_Type { + get { + return ResourceManager.GetString("ChatBot.AutoDig.List_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "distance" or "index", When using the "fixedpos" mode, the blocks are determined by distance to the player, or by the order in the list.. + /// + internal static string ChatBot_AutoDig_Location_Order { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Location_Order", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The position of the blocks when using "fixedpos" or "both" mode.. + /// + internal static string ChatBot_AutoDig_Locations { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Locations", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to output logs when digging blocks.. + /// + internal static string ChatBot_AutoDig_Log_Block_Dig { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Log_Block_Dig", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "lookat", "fixedpos" or "both". Digging the block being looked at, the block in a fixed position, or the block that needs to be all met.. + /// + internal static string ChatBot_AutoDig_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoDig.Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically drop items in inventory + ///You need to enable Inventory Handling to use this bot + ///See this file for an up-to-date list of item types you can use with this bot: https://mccteam.github.io/r/item/#L12. + /// + internal static string ChatBot_AutoDrop { + get { + return ResourceManager.GetString("ChatBot.AutoDrop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "include", "exclude" or "everything". Include: drop item IN the list. Exclude: drop item NOT IN the list. + /// + internal static string ChatBot_AutoDrop_Mode { + get { + return ResourceManager.GetString("ChatBot.AutoDrop.Mode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically eat food when your Hunger value is low + ///You need to enable Inventory Handling to use this bot. + /// + internal static string ChatBot_AutoEat { + get { + return ResourceManager.GetString("ChatBot.AutoEat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically catch fish using a fishing rod + ///Guide: https://mccteam.github.io/g/bots/#auto-fishing + ///You can use "/fish" to control the bot manually. + /// /!\ Make sure server rules allow automated farming before using this bot. + /// + internal static string ChatBot_AutoFishing { + get { + return ResourceManager.GetString("ChatBot.AutoFishing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep it as false if you have not changed it before.. + /// + internal static string ChatBot_AutoFishing_Antidespawn { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Antidespawn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch to a new rod from inventory after the current rod is unavailable.. + /// + internal static string ChatBot_AutoFishing_Auto_Rod_Switch { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Rod_Switch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to start fishing automatically after entering a world.. + /// + internal static string ChatBot_AutoFishing_Auto_Start { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Auto_Start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How soon to re-cast after successful fishing.. + /// + internal static string ChatBot_AutoFishing_Cast_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Cast_Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Will not use rods with less durability than this (full durability is 64). Set to zero to disable this feature.. + /// + internal static string ChatBot_AutoFishing_Durability_Limit { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Durability_Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This allows the player to change position/facing after each fish caught.. + /// + internal static string ChatBot_AutoFishing_Enable_Move { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Enable_Move", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long after entering the game to start fishing (seconds).. + /// + internal static string ChatBot_AutoFishing_Fishing_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fishing timeout (seconds). Timeout will trigger a re-cast.. + /// + internal static string ChatBot_AutoFishing_Fishing_Timeout { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Fishing_Timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.. + /// + internal static string ChatBot_AutoFishing_Hook_Threshold { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Hook_Threshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet.. + /// + internal static string ChatBot_AutoFishing_Log_Fish_Bobber { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Log_Fish_Bobber", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use the mainhand or the offhand to hold the rod.. + /// + internal static string ChatBot_AutoFishing_Mainhand { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Mainhand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to It will move in order "1->2->3->4->3->2->1->2->..." and can change position or facing or both each time. It is recommended to change the facing only.. + /// + internal static string ChatBot_AutoFishing_Movements { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Movements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hook movement in the X and Z axis less than this value will be considered stationary.. + /// + internal static string ChatBot_AutoFishing_Stationary_Threshold { + get { + return ResourceManager.GetString("ChatBot.AutoFishing.Stationary_Threshold", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically relog when disconnected by server, for example because the server is restating + /// /!\ Use Ignore_Kick_Message=true at own risk! Server staff might not appreciate if you auto-relog on manual kicks. + /// + internal static string ChatBot_AutoRelog { + get { + return ResourceManager.GetString("ChatBot.AutoRelog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The delay time before joining the server. (in seconds). + /// + internal static string ChatBot_AutoRelog_Delay { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When set to true, autorelog will reconnect regardless of kick messages.. + /// + internal static string ChatBot_AutoRelog_Ignore_Kick_Message { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Ignore_Kick_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If the kickout message matches any of the strings, then autorelog will be triggered.. + /// + internal static string ChatBot_AutoRelog_Kick_Messages { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Kick_Messages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Retries when failing to relog to the server. use -1 for unlimited retries.. + /// + internal static string ChatBot_AutoRelog_Retries { + get { + return ResourceManager.GetString("ChatBot.AutoRelog.Retries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run commands or send messages automatically when a specified pattern is detected in chat + ///Server admins can spoof chat messages (/nick, /tellraw) so keep this in mind when implementing AutoRespond rules + /// /!\ This bot may get spammy depending on your rules, although the global messagecooldown setting can help you avoiding accidental spam. + /// + internal static string ChatBot_AutoRespond { + get { + return ResourceManager.GetString("ChatBot.AutoRespond", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not remove colors from text (Note: Your matches will have to include color codes (ones using the § character) in order to work). + /// + internal static string ChatBot_AutoRespond_Match_Colors { + get { + return ResourceManager.GetString("ChatBot.AutoRespond.Match_Colors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logs chat messages in a file on disk.. + /// + internal static string ChatBot_ChatLog { + get { + return ResourceManager.GetString("ChatBot.ChatLog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This bot allows you to send and recieve messages and commands via a Discord channel. + ///For Setup you can either use the documentation or read here (Documentation has images). + ///Documentation: https://mccteam.github.io/g/bots/#discord-bridge + ///Setup: + ///First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA . + /// /!\ IMPORTANT /!\: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent [rest of string was truncated]";. + /// + internal static string ChatBot_DiscordBridge { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ID of a channel where you want to interact with the MCC using the bot.. + /// + internal static string ChatBot_DiscordBridge_ChannelId { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.ChannelId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message formats + ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! + ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. + ///For Discord message formatting, check the following: https://mccteam.github.io/r/dc-fmt.html. + /// + internal static string ChatBot_DiscordBridge_Formats { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.Formats", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The ID of a server/guild where you have invited the bot to.. + /// + internal static string ChatBot_DiscordBridge_GuildId { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.GuildId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to discord before canceling the task (minimum 1 second).. + /// + internal static string ChatBot_DiscordBridge_MessageSendTimeout { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.MessageSendTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A list of IDs of people you want to be able to interact with the MCC using the bot.. + /// + internal static string ChatBot_DiscordBridge_OwnersIds { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.OwnersIds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord Bot token.. + /// + internal static string ChatBot_DiscordBridge_Token { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.Token", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat.. + /// + internal static string ChatBot_DiscordBridge_AllowOtherBotMessages { + get { + return ResourceManager.GetString("ChatBot.DiscordBridge.AllowOtherBotMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically farms cropsfor you (plants, breaks and bonemeals them). + ///Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat. + ///Usage: "/farmer start" command and "/farmer stop" command. + ///NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes. + ///or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this. [rest of string was truncated]";. + /// + internal static string ChatBot_Farmer { + get { + return ResourceManager.GetString("ChatBot.Farmer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delay between tasks in seconds (Minimum 1 second). + /// + internal static string ChatBot_Farmer_Delay_Between_Tasks { + get { + return ResourceManager.GetString("ChatBot.Farmer.Delay_Between_Tasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enabled you to make the bot follow you + ///NOTE: This is an experimental feature, the bot can be slow at times, you need to walk with a normal speed and to sometimes stop for it to be able to keep up with you + ///It's similar to making animals follow you when you're holding food in your hand. + ///This is due to a slow pathfinding algorithm, we're working on getting a better one + ///You can tweak the update limit and find what works best for you. (NOTE: Do not but a very low one, because you might achieve the opposite, + /// [rest of string was truncated]";. + /// + internal static string ChatBot_FollowPlayer { + get { + return ResourceManager.GetString("ChatBot.FollowPlayer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Do not follow the player if he is in the range of 3 blocks (prevents the bot from pushing a player in an infinite loop). + /// + internal static string ChatBot_FollowPlayer_Stop_At_Distance { + get { + return ResourceManager.GetString("ChatBot.FollowPlayer.Stop_At_Distance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The rate at which the bot does calculations (in seconds) (You can tweak this if you feel the bot is too slow). + /// + internal static string ChatBot_FollowPlayer_Update_Limit { + get { + return ResourceManager.GetString("ChatBot.FollowPlayer.Update_Limit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A small game to demonstrate chat interactions. Players can guess mystery words one letter at a time. + ///You need to have ChatFormat working correctly and add yourself in botowners to start the game with /tell <bot username> start + /// /!\ This bot may get a bit spammy if many players are interacting with it. + /// + internal static string ChatBot_HangmanGame { + get { + return ResourceManager.GetString("ChatBot.HangmanGame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A Chat Bot that collects items on the ground. + /// + internal static string ChatBot_ItemsCollector { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If set to true, the bot will return to it's starting position after there are no items to collect. + /// + internal static string ChatBot_ItemsCollector_Always_Return_To_Start { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Always_Return_To_Start", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If set to true, the bot will collect all items, regardless of their type. If you want to use the whitelisted item types, disable this by setting it to false. + /// + internal static string ChatBot_ItemsCollector_Collect_All_Item_Types { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Collect_All_Item_Types", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The radius in which bot will look for items to collect (Default: 30). + /// + internal static string ChatBot_ItemsCollector_Collection_Radius { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Collection_Radius", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delay in milliseconds between bot scanning items (Recommended: 300-500). + /// + internal static string ChatBot_ItemsCollector_Delay_Between_Tasks { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Delay_Between_Tasks", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to In this list you can specify which items the bot will collect. To enable this, set the Collect_All_Item_Types to false. (NOTE: This does not prevent the bot from accidentally picking up other items, it only goes to positions where it finds the whitelisted items)\nYou can see the list of item types here: https://raw.githubusercontent.com/MCCTeam/Minecraft-Console-Client/master/MinecraftClient/Inventory/ItemType.cs. + /// + internal static string ChatBot_ItemsCollector_Items_Whitelist { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Items_Whitelist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If set to true, the bot will go after clustered items instead for the closest ones. + /// + internal static string ChatBot_ItemsCollector_Prioritize_Clusters { + get { + return ResourceManager.GetString("ChatBot.ItemsCollector.Prioritize_Clusters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info. + ///Setup: + ///1. Go to https://discord.com/developers/applications and log in with your Discord account. [rest of string was truncated]";. + /// + internal static string ChatBot_DiscordRpc { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Discord Application ID.. + /// + internal static string ChatBot_DiscordRpc_ApplicationId { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ApplicationId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The top line of the Rich Presence display. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_PresenceDetails { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceDetails", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The second line of the Rich Presence display. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_PresenceState { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key of the large image asset uploaded to your Discord application.. + /// + internal static string ChatBot_DiscordRpc_LargeImageKey { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tooltip text for the large image. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_LargeImageText { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The key of the small image asset uploaded to your Discord application (leave empty to hide).. + /// + internal static string ChatBot_DiscordRpc_SmallImageKey { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageKey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tooltip text for the small image. Supports placeholders.. + /// + internal static string ChatBot_DiscordRpc_SmallImageText { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the server address (host and port) in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowServerAddress { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowServerAddress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the player coordinates in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowCoordinates { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowCoordinates", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show health and food level in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowHealth { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowHealth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the current dimension in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowDimension { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowDimension", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the current gamemode in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowGamemode { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowGamemode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show elapsed session time in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowElapsedTime { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowElapsedTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show the online player count as a party size in the Discord presence.. + /// + internal static string ChatBot_DiscordRpc_ShowPlayerCount { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.ShowPlayerCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How often (in seconds) to refresh the Discord presence. Minimum: 1. + /// + internal static string ChatBot_DiscordRpc_UpdateIntervalSeconds { + get { + return ResourceManager.GetString("ChatBot.DiscordRpc.UpdateIntervalSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Relay messages between players and servers, like a mail plugin + ///This bot can store messages when the recipients are offline, and send them when they join the server + /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable this bot only if you trust server admins. + /// + internal static string ChatBot_Mailer { + get { + return ResourceManager.GetString("ChatBot.Mailer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows you to render maps in the console and into images (which can be then sent to Discord using Discord Bridge Chat Bot) + ///This is useful for solving captchas which use maps + ///The maps are rendered into Rendered_Maps folder if the Save_To_File is enabled. + ///NOTE: + ///If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update to see them immediatelly in the console. + /// /!\ Make sure server rules allow bots to be used on the server, or you risk being punished.. + /// + internal static string ChatBot_Map { + get { + return ResourceManager.GetString("ChatBot.Map", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Automatically render the map once it is received or updated from/by the server. + /// + internal static string ChatBot_Map_Auto_Render_On_Update { + get { + return ResourceManager.GetString("ChatBot.Map.Auto_Render_On_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Delete all rendered maps on unload/reload or when you launch the MCC again.. + /// + internal static string ChatBot_Map_Delete_All_On_Unload { + get { + return ResourceManager.GetString("ChatBot.Map.Delete_All_On_Unload", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get a notification when you have gotten a map from the server for the first time. + /// + internal static string ChatBot_Map_Notify_On_First_Update { + get { + return ResourceManager.GetString("ChatBot.Map.Notify_On_First_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord.. + /// + internal static string ChatBot_Map_Rasize_Rendered_Image { + get { + return ResourceManager.GetString("ChatBot.Map.Rasize_Rendered_Image", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to render the map in the console.. + /// + internal static string ChatBot_Map_Render_In_Console { + get { + return ResourceManager.GetString("ChatBot.Map.Render_In_Console", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The size that a rendered image should be resized to, in pixels (eg. 512).. + /// + internal static string ChatBot_Map_Resize_To { + get { + return ResourceManager.GetString("ChatBot.Map.Resize_To", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to store the rendered map as a file (You need this setting if you want to get a map on Discord using Discord Bridge).. + /// + internal static string ChatBot_Map_Save_To_File { + get { + return ResourceManager.GetString("ChatBot.Map.Save_To_File", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send a rendered map (saved to a file) to a Discord or a Telegram channel via the Discord or Telegram Bride chat bot (The Discord/Telegram Bridge chat bot must be enabled and configured!) + ///You need to enable Save_To_File in order for this to work. + ///We also recommend turning on resizing.. + /// + internal static string ChatBot_Map_Send_Rendered_To_Bridges { + get { + return ResourceManager.GetString("ChatBot.Map.Send_Rendered_To_Bridges", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log the list of players periodically into a textual file.. + /// + internal static string ChatBot_PlayerListLogger { + get { + return ResourceManager.GetString("ChatBot.PlayerListLogger", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to (In seconds). + /// + internal static string ChatBot_PlayerListLogger_Delay { + get { + return ResourceManager.GetString("ChatBot.PlayerListLogger.Delay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Send MCC console commands to your bot through server PMs (/tell) + ///You need to have ChatFormat working correctly and add yourself in botowners to use the bot + /// /!\ Server admins can spoof PMs (/tellraw, /nick) so enable RemoteControl only if you trust server admins. + /// + internal static string ChatBot_RemoteControl { + get { + return ResourceManager.GetString("ChatBot.RemoteControl", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable recording of the game (/replay start) and replay it later using the Replay Mod (https://www.replaymod.com/) + ///Please note that due to technical limitations, the client player (you) will not be shown in the replay file + /// /!\ You SHOULD use /replay stop or exit the program gracefully with /quit OR THE REPLAY FILE MAY GET CORRUPT!. + /// + internal static string ChatBot_ReplayCapture { + get { + return ResourceManager.GetString("ChatBot.ReplayCapture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long should replay file be auto-saved, in seconds. Use -1 to disable.. + /// + internal static string ChatBot_ReplayCapture_Backup_Interval { + get { + return ResourceManager.GetString("ChatBot.ReplayCapture.Backup_Interval", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Schedule commands and scripts to launch on various events such as server join, date/time or time interval + ///See https://mccteam.github.io/g/bots/#script-scheduler for more info. + /// + internal static string ChatBot_ScriptScheduler { + get { + return ResourceManager.GetString("ChatBot.ScriptScheduler", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel. + /// /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel. + ///----------------------------------------------------------- + ///Setup: + ///First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather + ///Click on "Start" button and re [rest of string was truncated]";. + /// + internal static string ChatBot_TelegramBridge { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A list of Chat IDs that are allowed to send messages and execute commands. To get an id of your chat DM with the bot use ".chatid" bot command in Telegram.. + /// + internal static string ChatBot_TelegramBridge_Authorized_Chat_Ids { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.Authorized_Chat_Ids", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ID of a channel where you want to interact with the MCC using the bot.. + /// + internal static string ChatBot_TelegramBridge_ChannelId { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.ChannelId", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Message formats + ///Words wrapped with { and } are going to be replaced during the code execution, do not change them! + ///For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time. + ///For Telegram message formatting, check the following: https://mccteam.github.io/r/tg-fmt.html. + /// + internal static string ChatBot_TelegramBridge_Formats { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.Formats", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second).. + /// + internal static string ChatBot_TelegramBridge_MessageSendTimeout { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.MessageSendTimeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Your Telegram Bot token.. + /// + internal static string ChatBot_TelegramBridge_Token { + get { + return ResourceManager.GetString("ChatBot.TelegramBridge.Token", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remotely control the client using Web Sockets.\n# This is useful if you want to implement an application that can remotely and asynchronously execute procedures in MCC.\n# Example implementation written in JavaScript: https://github.com/milutinke/MCC.js.git\n# The protocol specification will be available in the documentation soon.. + /// + internal static string ChatBot_WebSocketBot { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allow IP aliases, such as "localhost" or if using containers then the container name can be used.... + /// + internal static string ChatBot_WebSocketBot_AllowIpAlias { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.AllowIpAlias", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This setting is for developers who are developing a library that uses this chat bot to remotely execute procedures/commands/functions.. + /// + internal static string ChatBot_WebSocketBot_DebugMode { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.DebugMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The IP address that Websocket server will be bound to.. + /// + internal static string ChatBot_WebSocketBot_Ip { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.Ip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A password that will be used to authenticate on thw Websocket server (It is recommended to change the default password and to set a strong one).. + /// + internal static string ChatBot_WebSocketBot_Password { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Port that Websocket server will be bounded to.. + /// + internal static string ChatBot_WebSocketBot_Port { + get { + return ResourceManager.GetString("ChatBot.WebSocketBot.Port", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MCC does it best to detect chat messages, but some server have unusual chat formats + ///When this happens, you'll need to configure chat format below, see https://mccteam.github.io/g/conf/#chat-format-section. + /// + internal static string ChatFormat { + get { + return ResourceManager.GetString("ChatFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MCC support for common message formats. Set "false" to avoid conflicts with custom formats.. + /// + internal static string ChatFormat_Builtins { + get { + return ResourceManager.GetString("ChatFormat.Builtins", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to use the custom regular expressions below for detection.. + /// + internal static string ChatFormat_UserDefined { + get { + return ResourceManager.GetString("ChatFormat.UserDefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Console-related settings.. + /// + internal static string Console { + get { + return ResourceManager.GetString("Console", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The settings for command completion suggestions. + ///Custom colors are only available when using "vt100_24bit" color mode.. + /// + internal static string Console_CommandSuggestion { + get { + return ResourceManager.GetString("Console.CommandSuggestion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to display command suggestions in the console.. + /// + internal static string Console_CommandSuggestion_Enable { + get { + return ResourceManager.GetString("Console.CommandSuggestion.Enable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable this option if the arrows in the command suggestions are not displayed properly in your terminal.. + /// + internal static string Console_CommandSuggestion_Use_Basic_Arrow { + get { + return ResourceManager.GetString("Console.CommandSuggestion.Use_Basic_Arrow", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Console mode: "classic" for the standard terminal, "tui" for a pseudo-graphical full-screen interface.. + /// + internal static string Console_General_ConsoleMode { + get { + return ResourceManager.GetString("Console.General.ConsoleMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "disable", "legacy_4bit", "vt100_4bit", "vt100_8bit" or "vt100_24bit". If a garbled code like "←[0m" appears on the terminal, you can try switching to "legacy_4bit" mode, or just disable it.. + /// + internal static string Console_General_ConsoleColorMode { + get { + return ResourceManager.GetString("Console.General.ConsoleColorMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to display the MCC startup banner with version info and icon.. + /// + internal static string Console_General_Display_Icon_Banner { + get { + return ResourceManager.GetString("Console.General.Display_Icon_Banner", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You can use "Ctrl+P" to print out the current input and cursor position.. + /// + internal static string Console_General_Display_Input { + get { + return ResourceManager.GetString("Console.General.Display_Input", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum number of input history records to keep.. + /// + internal static string Console_General_History_Input_Records { + get { + return ResourceManager.GetString("Console.General.History_Input_Records", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum log lines kept in TUI mode scrollback. Set to 0 for automatic (3000 on x86/x64, 500 on ARM).. + /// + internal static string Console_General_TUI_Log_Scrollback { + get { + return ResourceManager.GetString("Console.General.TUI_Log_Scrollback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Startup Config File + ///Please do not record extraneous data in this file as it will be overwritten by MCC. + /// + ///New to Minecraft Console Client? Check out this document: https://mccteam.github.io/g/conf.html + ///Want to upgrade to a newer version? See https://github.com/MCCTeam/Minecraft-Console-Client/#download. + /// + internal static string Head { + get { + return ResourceManager.GetString("Head", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This setting affects only the messages in the console.. + /// + internal static string Logging { + get { + return ResourceManager.GetString("Logging", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regex for filtering chat message.. + /// + internal static string Logging_ChatFilter { + get { + return ResourceManager.GetString("Logging.ChatFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show server chat messages.. + /// + internal static string Logging_ChatMessages { + get { + return ResourceManager.GetString("Logging.ChatMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Regex for filtering debug message.. + /// + internal static string Logging_DebugFilter { + get { + return ResourceManager.GetString("Logging.DebugFilter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enable this before submitting bug reports. Thanks!. + /// + internal static string Logging_DebugMessages { + get { + return ResourceManager.GetString("Logging.DebugMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show error messages.. + /// + internal static string Logging_ErrorMessages { + get { + return ResourceManager.GetString("Logging.ErrorMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to "disable" or "blacklist" OR "whitelist". Blacklist hide message match regex. Whitelist show message match regex.. + /// + internal static string Logging_FilterMode { + get { + return ResourceManager.GetString("Logging.FilterMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Informative messages. (i.e Most of the message from MCC). + /// + internal static string Logging_InfoMessages { + get { + return ResourceManager.GetString("Logging.InfoMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log file name.. + /// + internal static string Logging_LogFile { + get { + return ResourceManager.GetString("Logging.LogFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Write log messages to file.. + /// + internal static string Logging_LogToFile { + get { + return ResourceManager.GetString("Logging.LogToFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prepend timestamp to messages in log file.. + /// + internal static string Logging_PrependTimestamp { + get { + return ResourceManager.GetString("Logging.PrependTimestamp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Keep color codes in the saved text.(look like "§b"). + /// + internal static string Logging_SaveColorCodes { + get { + return ResourceManager.GetString("Logging.SaveColorCodes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show warning messages.. + /// + internal static string Logging_WarningMessages { + get { + return ResourceManager.GetString("Logging.WarningMessages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make sure you understand what each setting does before changing anything!. + /// + internal static string Main_Advanced { + get { + return ResourceManager.GetString("Main.Advanced", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to AccountList: It allows a fast account switching without directly using the credentials + ///Usage examples: "/tell <mybot> reco Player2", "/connect <serverip> Player1". + /// + internal static string Main_Advanced_account_list { + get { + return ResourceManager.GetString("Main.Advanced.account_list", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle auto respawn if client player was dead (make sure your spawn point is safe).. + /// + internal static string Main_Advanced_auto_respawn { + get { + return ResourceManager.GetString("Main.Advanced.auto_respawn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set the owner of the bot. /!\ Server admins can impersonate owners!. + /// + internal static string Main_Advanced_bot_owners { + get { + return ResourceManager.GetString("Main.Advanced.bot_owners", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "mcc", "vanilla" or "none". This is how MCC identifies itself to the server.. + /// + internal static string Main_Advanced_brand_info { + get { + return ResourceManager.GetString("Main.Advanced.brand_info", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Leave empty for no logfile.. + /// + internal static string Main_Advanced_chatbot_log_file { + get { + return ResourceManager.GetString("Main.Advanced.chatbot_log_file", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If turned off, the emoji will be replaced with a simpler character (for /chunk status).. + /// + internal static string Main_Advanced_enable_emoji { + get { + return ResourceManager.GetString("Main.Advanced.enable_emoji", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set to false to opt-out of Sentry error logging.. + /// + internal static string Main_Advanced_enable_sentry { + get { + return ResourceManager.GetString("Main.Advanced.enable_sentry", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle entity handling.. + /// + internal static string Main_Advanced_entity_handling { + get { + return ResourceManager.GetString("Main.Advanced.entity_handling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to exit directly when an error occurs, for using MCC in non-interactive scripts.. + /// + internal static string Main_Advanced_exit_on_failure { + get { + return ResourceManager.GetString("Main.Advanced.exit_on_failure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Ignore invalid player name. + /// + internal static string Main_Advanced_ignore_invalid_playername { + get { + return ResourceManager.GetString("Main.Advanced.ignore_invalid_playername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "none", "slash"(/) or "backslash"(\).. + /// + internal static string Main_Advanced_internal_cmd_char { + get { + return ResourceManager.GetString("Main.Advanced.internal_cmd_char", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle inventory handling.. + /// + internal static string Main_Advanced_inventory_handling { + get { + return ResourceManager.GetString("Main.Advanced.inventory_handling", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fill in with in-game locale code, check https://mccteam.github.io/r/l-code.html. + /// + internal static string Main_Advanced_language { + get { + return ResourceManager.GetString("Main.Advanced.language", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Load translations applied to MCC when available, turn it off to use English only.. + /// + internal static string Main_Advanced_LoadMccTrans { + get { + return ResourceManager.GetString("Main.Advanced.LoadMccTrans", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "auto", "no" or "force". Force-enabling only works for MC 1.13+.. + /// + internal static string Main_Advanced_mc_forge { + get { + return ResourceManager.GetString("Main.Advanced.mc_forge", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "auto" or "1.X.X" values. Allows to skip server info retrieval.. + /// + internal static string Main_Advanced_mc_version { + get { + return ResourceManager.GetString("Main.Advanced.mc_version", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Controls the minimum interval (in seconds) between sending each message to the server.. + /// + internal static string Main_Advanced_message_cooldown { + get { + return ResourceManager.GetString("Main.Advanced.message_cooldown", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Override the maximum chat message length. Set to 0 to use the default (100 for 1.10 and below, 256 for 1.11+). WARNING: Setting this incorrectly may cause you to be kicked from the server.. + /// + internal static string Main_Advanced_max_chat_message_length { + get { + return ResourceManager.GetString("Main.Advanced.max_chat_message_length", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable support for joining Minecraft Realms worlds.. + /// + internal static string Main_Advanced_minecraft_realms { + get { + return ResourceManager.GetString("Main.Advanced.minecraft_realms", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimum height to use when calculating the image size from the height of the terminal.. + /// + internal static string Main_Advanced_MinTerminalHeight { + get { + return ResourceManager.GetString("Main.Advanced.MinTerminalHeight", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimum width used when calculating the image size from the width of the terminal.. + /// + internal static string Main_Advanced_MinTerminalWidth { + get { + return ResourceManager.GetString("Main.Advanced.MinTerminalWidth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Enable head movement while walking to avoid anti-cheat triggers.. + /// + internal static string Main_Advanced_move_head_while_walking { + get { + return ResourceManager.GetString("Main.Advanced.move_head_while_walking", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A movement speed higher than 2 may be considered cheating.. + /// + internal static string Main_Advanced_movement_speed { + get { + return ResourceManager.GetString("Main.Advanced.movement_speed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only works on Windows XP-8 or Windows 10 with old console.. + /// + internal static string Main_Advanced_player_head_icon { + get { + return ResourceManager.GetString("Main.Advanced.player_head_icon", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For remote control of the bot.. + /// + internal static string Main_Advanced_private_msgs_cmd_name { + get { + return ResourceManager.GetString("Main.Advanced.private_msgs_cmd_name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How to retain profile key. Use "none", "memory" or "disk".. + /// + internal static string Main_Advanced_profilekey_cache { + get { + return ResourceManager.GetString("Main.Advanced.profilekey_cache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "no", "fast" (5s timeout), or "yes". Required for joining some servers.. + /// + internal static string Main_Advanced_resolve_srv_records { + get { + return ResourceManager.GetString("Main.Advanced.resolve_srv_records", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cache compiled scripts for faster load on low-end devices.. + /// + internal static string Main_Advanced_script_cache { + get { + return ResourceManager.GetString("Main.Advanced.script_cache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ServerList: It allows an easier and faster server switching with short aliases instead of full server IP + ///Aliases cannot contain dots or spaces, and the name "localhost" cannot be used as an alias. + ///Usage examples: "/tell <mybot> connect Server1", "/connect Server2". + /// + internal static string Main_Advanced_server_list { + get { + return ResourceManager.GetString("Main.Advanced.server_list", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to How to retain session tokens. Use "none", "memory" or "disk".. + /// + internal static string Main_Advanced_session_cache { + get { + return ResourceManager.GetString("Main.Advanced.session_cache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Decode links embedded in chat messages and show them in console.. + /// + internal static string Main_Advanced_show_chat_links { + get { + return ResourceManager.GetString("Main.Advanced.show_chat_links", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show inventory layout as ASCII art in inventory command.. + /// internal static string Main_Advanced_show_inventory_layout { get { return ResourceManager.GetString("Main.Advanced.show_inventory_layout", resourceCulture); @@ -1862,345 +1890,345 @@ namespace MinecraftClient { /// Looks up a localized string similar to System messages for server ops.. /// internal static string Main_Advanced_show_system_messages { - get { - return ResourceManager.GetString("Main.Advanced.show_system_messages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Messages displayed above xp bar, set this to false in case of xp bar spam.. - /// - internal static string Main_Advanced_show_xpbar_messages { - get { - return ResourceManager.GetString("Main.Advanced.show_xpbar_messages", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Temporary fix for Badpacket issue on some servers. Need to enable "TerrainAndMovements" first.. - /// - internal static string Main_Advanced_temporary_fix_badpacket { - get { - return ResourceManager.GetString("Main.Advanced.temporary_fix_badpacket", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Uses more ram, cpu, bandwidth but allows you to move around.. - /// - internal static string Main_Advanced_terrain_and_movements { - get { - return ResourceManager.GetString("Main.Advanced.terrain_and_movements", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Customize the TCP connection timeout with the server. (in seconds). - /// - internal static string Main_Advanced_timeout { - get { - return ResourceManager.GetString("Main.Advanced.timeout", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Prepend timestamps to chat messages.. - /// - internal static string Main_Advanced_timestamps { - get { - return ResourceManager.GetString("Main.Advanced.timestamps", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Login=Email or Name. Use "-" as password for offline mode. Leave blank to prompt user on startup.. - /// - internal static string Main_General_account { - get { - return ResourceManager.GetString("Main.General.account", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yggdrasil authlib server domain name and port.. - /// - internal static string Main_General_AuthlibServer { - get { - return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yggdrasil authlib multi-user selection.. - /// - internal static string Main_General_AuthlibUser { - get { - return ResourceManager.GetString("Main.General.AuthlibUser", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The address of the game server, "Host" can be filled in with domain name or IP address. (The "Port" field can be deleted, it will be resolved automatically). - /// - internal static string Main_General_login { - get { - return ResourceManager.GetString("Main.General.login", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Microsoft Account sign-in method: "mcc" (device code, supports 2FA) OR "browser" (manual browser login).. - /// - internal static string Main_General_method { - get { - return ResourceManager.GetString("Main.General.method", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Account type: "mojang" OR "microsoft" OR "yggdrasil". Also affects interactive login in console.. - /// - internal static string Main_General_server_info { - get { - return ResourceManager.GetString("Main.General.server_info", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Settings below are sent to the server and only affect server-side things like your skin.. - /// - internal static string MCSettings { - get { - return ResourceManager.GetString("MCSettings", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Allows disabling chat colors server-side.. - /// - internal static string MCSettings_ChatColors { - get { - return ResourceManager.GetString("MCSettings.ChatColors", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use "enabled", "commands", or "disabled". Allows to mute yourself.... - /// - internal static string MCSettings_ChatMode { - get { - return ResourceManager.GetString("MCSettings.ChatMode", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MC 1.7- difficulty. "peaceful", "easy", "normal", "difficult".. - /// - internal static string MCSettings_Difficulty { - get { - return ResourceManager.GetString("MCSettings.Difficulty", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to If disabled, settings below are not sent to the server.. - /// - internal static string MCSettings_Enabled { - get { - return ResourceManager.GetString("MCSettings.Enabled", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use any language implemented in Minecraft.. - /// - internal static string MCSettings_Locale { - get { - return ResourceManager.GetString("MCSettings.Locale", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MC 1.9+ main hand. "left" or "right".. - /// - internal static string MCSettings_MainHand { - get { - return ResourceManager.GetString("MCSettings.MainHand", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Value range: [0 - 255].. - /// - internal static string MCSettings_RenderDistance { - get { - return ResourceManager.GetString("MCSettings.RenderDistance", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly - ///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy. - ///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server. - /// /!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!. - /// - internal static string Proxy { - get { - return ResourceManager.GetString("Proxy", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to connect to the game server through a proxy.. - /// - internal static string Proxy_Enabled_Ingame { - get { - return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to connect to the login server through a proxy.. - /// - internal static string Proxy_Enabled_Login { - get { - return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to download MCC updates via proxy.. - /// - internal static string Proxy_Enabled_Update { - get { - return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only required for password-protected proxies.. - /// - internal static string Proxy_Password { - get { - return ResourceManager.GetString("Proxy.Password", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Supported types: "HTTP", "SOCKS4", "SOCKS4a", "SOCKS5".. - /// - internal static string Proxy_Proxy_Type { - get { - return ResourceManager.GetString("Proxy.Proxy_Type", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Proxy server must allow HTTPS for login, and non-443 ports for playing.. - /// - internal static string Proxy_Server { - get { - return ResourceManager.GetString("Proxy.Server", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only required for password-protected proxies.. - /// - internal static string Proxy_Username { - get { - return ResourceManager.GetString("Proxy.Username", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Chat signature related settings (affects minecraft 1.19+). - /// - internal static string Signature { - get { - return ResourceManager.GetString("Signature", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Microsoft accounts only. If disabled, will not be able to sign chat and join servers configured with "enforce-secure-profile=true". - /// - internal static string Signature_LoginWithSecureProfile { - get { - return ResourceManager.GetString("Signature.LoginWithSecureProfile", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use red    color block to mark chat without legitimate signature. - /// - internal static string Signature_MarkIllegallySignedMsg { - get { - return ResourceManager.GetString("Signature.MarkIllegallySignedMsg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use green  color block to mark chat with legitimate signatures. - /// - internal static string Signature_MarkLegallySignedMsg { - get { - return ResourceManager.GetString("Signature.MarkLegallySignedMsg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use yellow color block to mark chat that have been modified by the server.. - /// - internal static string Signature_MarkModifiedMsg { - get { - return ResourceManager.GetString("Signature.MarkModifiedMsg", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Use gray   color block to mark system message (always without signature). - /// - internal static string Signature_MarkSystemMessage { - get { - return ResourceManager.GetString("Signature.MarkSystemMessage", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to display chat and messages in commands without legal signatures. - /// - internal static string Signature_ShowIllegalSignedChat { - get { - return ResourceManager.GetString("Signature.ShowIllegalSignedChat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Set to true to display messages modified by the server, false to display the original signed messages. - /// - internal static string Signature_ShowModifiedChat { - get { - return ResourceManager.GetString("Signature.ShowModifiedChat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to sign the chat send from MCC. - /// - internal static string Signature_SignChat { - get { - return ResourceManager.GetString("Signature.SignChat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Whether to sign the messages contained in the commands sent by MCC. For example, the message in "/msg" and "/me". - /// - internal static string Signature_SignMessageInCommand { - get { - return ResourceManager.GetString("Signature.SignMessageInCommand", resourceCulture); - } - } - } -} + get { + return ResourceManager.GetString("Main.Advanced.show_system_messages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Messages displayed above xp bar, set this to false in case of xp bar spam.. + /// + internal static string Main_Advanced_show_xpbar_messages { + get { + return ResourceManager.GetString("Main.Advanced.show_xpbar_messages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Temporary fix for Badpacket issue on some servers. Need to enable "TerrainAndMovements" first.. + /// + internal static string Main_Advanced_temporary_fix_badpacket { + get { + return ResourceManager.GetString("Main.Advanced.temporary_fix_badpacket", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Uses more ram, cpu, bandwidth but allows you to move around.. + /// + internal static string Main_Advanced_terrain_and_movements { + get { + return ResourceManager.GetString("Main.Advanced.terrain_and_movements", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Customize the TCP connection timeout with the server. (in seconds). + /// + internal static string Main_Advanced_timeout { + get { + return ResourceManager.GetString("Main.Advanced.timeout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Prepend timestamps to chat messages.. + /// + internal static string Main_Advanced_timestamps { + get { + return ResourceManager.GetString("Main.Advanced.timestamps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login=Email or Name. Use "-" as password for offline mode. Leave blank to prompt user on startup.. + /// + internal static string Main_General_account { + get { + return ResourceManager.GetString("Main.General.account", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yggdrasil authlib server domain name and port.. + /// + internal static string Main_General_AuthlibServer { + get { + return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yggdrasil authlib multi-user selection.. + /// + internal static string Main_General_AuthlibUser { + get { + return ResourceManager.GetString("Main.General.AuthlibUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The address of the game server, "Host" can be filled in with domain name or IP address. (The "Port" field can be deleted, it will be resolved automatically). + /// + internal static string Main_General_login { + get { + return ResourceManager.GetString("Main.General.login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft Account sign-in method: "mcc" (device code, supports 2FA) OR "browser" (manual browser login).. + /// + internal static string Main_General_method { + get { + return ResourceManager.GetString("Main.General.method", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account type: "mojang" OR "microsoft" OR "yggdrasil". Also affects interactive login in console.. + /// + internal static string Main_General_server_info { + get { + return ResourceManager.GetString("Main.General.server_info", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings below are sent to the server and only affect server-side things like your skin.. + /// + internal static string MCSettings { + get { + return ResourceManager.GetString("MCSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Allows disabling chat colors server-side.. + /// + internal static string MCSettings_ChatColors { + get { + return ResourceManager.GetString("MCSettings.ChatColors", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use "enabled", "commands", or "disabled". Allows to mute yourself.... + /// + internal static string MCSettings_ChatMode { + get { + return ResourceManager.GetString("MCSettings.ChatMode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MC 1.7- difficulty. "peaceful", "easy", "normal", "difficult".. + /// + internal static string MCSettings_Difficulty { + get { + return ResourceManager.GetString("MCSettings.Difficulty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If disabled, settings below are not sent to the server.. + /// + internal static string MCSettings_Enabled { + get { + return ResourceManager.GetString("MCSettings.Enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use any language implemented in Minecraft.. + /// + internal static string MCSettings_Locale { + get { + return ResourceManager.GetString("MCSettings.Locale", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MC 1.9+ main hand. "left" or "right".. + /// + internal static string MCSettings_MainHand { + get { + return ResourceManager.GetString("MCSettings.MainHand", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value range: [0 - 255].. + /// + internal static string MCSettings_RenderDistance { + get { + return ResourceManager.GetString("MCSettings.RenderDistance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly + ///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy. + ///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server. + /// /!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!. + /// + internal static string Proxy { + get { + return ResourceManager.GetString("Proxy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to connect to the game server through a proxy.. + /// + internal static string Proxy_Enabled_Ingame { + get { + return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to connect to the login server through a proxy.. + /// + internal static string Proxy_Enabled_Login { + get { + return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to download MCC updates via proxy.. + /// + internal static string Proxy_Enabled_Update { + get { + return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only required for password-protected proxies.. + /// + internal static string Proxy_Password { + get { + return ResourceManager.GetString("Proxy.Password", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Supported types: "HTTP", "SOCKS4", "SOCKS4a", "SOCKS5".. + /// + internal static string Proxy_Proxy_Type { + get { + return ResourceManager.GetString("Proxy.Proxy_Type", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Proxy server must allow HTTPS for login, and non-443 ports for playing.. + /// + internal static string Proxy_Server { + get { + return ResourceManager.GetString("Proxy.Server", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only required for password-protected proxies.. + /// + internal static string Proxy_Username { + get { + return ResourceManager.GetString("Proxy.Username", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chat signature related settings (affects minecraft 1.19+). + /// + internal static string Signature { + get { + return ResourceManager.GetString("Signature", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Microsoft accounts only. If disabled, will not be able to sign chat and join servers configured with "enforce-secure-profile=true". + /// + internal static string Signature_LoginWithSecureProfile { + get { + return ResourceManager.GetString("Signature.LoginWithSecureProfile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use red    color block to mark chat without legitimate signature. + /// + internal static string Signature_MarkIllegallySignedMsg { + get { + return ResourceManager.GetString("Signature.MarkIllegallySignedMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use green  color block to mark chat with legitimate signatures. + /// + internal static string Signature_MarkLegallySignedMsg { + get { + return ResourceManager.GetString("Signature.MarkLegallySignedMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use yellow color block to mark chat that have been modified by the server.. + /// + internal static string Signature_MarkModifiedMsg { + get { + return ResourceManager.GetString("Signature.MarkModifiedMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use gray   color block to mark system message (always without signature). + /// + internal static string Signature_MarkSystemMessage { + get { + return ResourceManager.GetString("Signature.MarkSystemMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to display chat and messages in commands without legal signatures. + /// + internal static string Signature_ShowIllegalSignedChat { + get { + return ResourceManager.GetString("Signature.ShowIllegalSignedChat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set to true to display messages modified by the server, false to display the original signed messages. + /// + internal static string Signature_ShowModifiedChat { + get { + return ResourceManager.GetString("Signature.ShowModifiedChat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to sign the chat send from MCC. + /// + internal static string Signature_SignChat { + get { + return ResourceManager.GetString("Signature.SignChat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Whether to sign the messages contained in the commands sent by MCC. For example, the message in "/msg" and "/me". + /// + internal static string Signature_SignMessageInCommand { + get { + return ResourceManager.GetString("Signature.SignMessageInCommand", resourceCulture); + } + } + } +} diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx index b4d32711..a5e5e407 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx @@ -311,6 +311,21 @@ You can use "/fish" to control the bot manually. A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish. + + Enable fish bite detection using fishing bobber velocity packets. + + + Velocity Y threshold (blocks/tick). Values below this are treated as a bite. Keep this value negative. + + + Enable fish bite detection using splash sounds near the fishing bobber. + + + Maximum distance (blocks) between splash sound and bobber to treat it as a bite. + + + Delay (seconds) after bobber spawn before bite detection starts. Helps ignore cast-entry splash/motion. + Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet. @@ -393,6 +408,12 @@ For Discord message formatting, check the following: https://mccteam.github.io/r When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat. The bridge always ignores its own messages to prevent loops. + + When enabled, all text received from the Minecraft server (including system messages, join/leave notifications, etc.) will be relayed to Discord, not just player chat and private messages. + + + Interval in seconds to aggregate messages before sending them to Discord. When set to 0, messages are sent immediately one by one. When set to a value like 1.0, messages received within that interval are batched into a single Discord message. Useful for reducing Discord API rate limits. + Automatically farms crops for you (plants, breaks and bonemeals them). Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat. @@ -560,9 +581,18 @@ Custom colors are only available when using "vt100_24bit" color mode. Use "disable", "legacy_4bit", "vt100_4bit", "vt100_8bit" or "vt100_24bit". If a garbled code like "←[0m" appears on the terminal, you can try switching to "legacy_4bit" mode, or just disable it. + + Whether to display the MCC startup icon banner. + You can use "Ctrl+P" to print out the current input and cursor position. + + Maximum number of input history records to keep. + + + Maximum log lines kept in TUI mode scrollback. Set to 0 for automatic. + Startup Config File Please do not record extraneous data in this file as it will be overwritten by MCC. @@ -933,6 +963,42 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be Set to false to opt-out of Sentry error logging. + + Settings for the TUI minimap overlay that shows terrain and entities. + + + Whether the minimap is visible on startup in TUI mode. + + + Blocks per pixel, 1-16. 1 = closest (1:1), 16 = farthest (16 blocks per pixel). + + + Map width in pixels (characters). Range 10-120, default 40. + + + Map height in pixels (must be even, uses half-block chars). Range 4-80, default 40. + + + Minimap position: "top_left", "top_right", "center", "bottom_left", or "bottom_right". + + + Show player names on the minimap. + + + Show hostile mob names on the minimap. + + + Show neutral mob names on the minimap. + + + Show passive mob names on the minimap. + + + Minimap refresh interval in milliseconds (100-5000). + + + Cave rendering mode: "auto" (detect ceiling), "on" (always cave view), "off" (always surface view). + Yggdrasil authlib multi-user selection. diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs index dc8c3e4a..dd7c0d85 100644 --- a/MinecraftClient/Resources/Translations/Translations.Designer.cs +++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs @@ -437,6 +437,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Dropped low durability {0} from slot {1}.. + /// + internal static string bot_autodig_drop_low_durability { + get { + return ResourceManager.GetString("bot.autodig.drop_low_durability", resourceCulture); + } + } + /// /// Looks up a localized string similar to The block currently pointed to is not in the allowed list.. /// @@ -473,6 +482,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Switch to {0} from slot {1}.. + /// + internal static string bot_autodig_switch { + get { + return ResourceManager.GetString("bot.autodig.switch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Added item {0}. /// @@ -879,6 +897,24 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Waiting {0:0.000} seconds before reconnecting... ({1} retries left). + /// + internal static string bot_autoRelog_wait_with_retries { + get { + return ResourceManager.GetString("bot.autoRelog.wait_with_retries", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unlimited. + /// + internal static string bot_autoRelog_retries_unlimited { + get { + return ResourceManager.GetString("bot.autoRelog.retries_unlimited", resourceCulture); + } + } + /// /// Looks up a localized string similar to File not found: '{0}'. /// @@ -2269,6 +2305,78 @@ namespace MinecraftClient { } } + internal static string mcc_banner_classic { + get { + return ResourceManager.GetString("mcc.banner.classic", resourceCulture); + } + } + + internal static string mcc_banner_label_mc_versions { + get { + return ResourceManager.GetString("mcc.banner.label_mc_versions", resourceCulture); + } + } + + internal static string mcc_server_info_label_server { + get { + return ResourceManager.GetString("mcc.server_info.label_server", resourceCulture); + } + } + + internal static string mcc_server_info_label_version { + get { + return ResourceManager.GetString("mcc.server_info.label_version", resourceCulture); + } + } + + internal static string mcc_server_info_label_protocol { + get { + return ResourceManager.GetString("mcc.server_info.label_protocol", resourceCulture); + } + } + + internal static string mcc_server_info_label_players { + get { + return ResourceManager.GetString("mcc.server_info.label_players", resourceCulture); + } + } + + internal static string mcc_server_info_label_ping { + get { + return ResourceManager.GetString("mcc.server_info.label_ping", resourceCulture); + } + } + + internal static string mcc_server_info_label_ping_ms { + get { + return ResourceManager.GetString("mcc.server_info.label_ping_ms", resourceCulture); + } + } + + internal static string mcc_server_info_label_connecting_as { + get { + return ResourceManager.GetString("mcc.server_info.label_connecting_as", resourceCulture); + } + } + + internal static string mcc_server_info_label_online { + get { + return ResourceManager.GetString("mcc.server_info.label_online", resourceCulture); + } + } + + internal static string mcc_server_info_sample_more { + get { + return ResourceManager.GetString("mcc.server_info.sample_more", resourceCulture); + } + } + + internal static string mcc_server_info_version_upgrade { + get { + return ResourceManager.GetString("mcc.server_info.version_upgrade", resourceCulture); + } + } + /// /// Looks up a localized string similar to Converting session cache from disk: {0}. /// @@ -3558,6 +3666,51 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to quickly enable recommended features.. + /// + internal static string cmd_tryout_desc { + get { + return ResourceManager.GetString("cmd.tryout.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available quick actions:. + /// + internal static string cmd_tryout_list_header { + get { + return ResourceManager.GetString("cmd.tryout.list.header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to tui: set [Console.General] ConsoleMode = "tui" for the next restart.. + /// + internal static string cmd_tryout_list_tui { + get { + return ResourceManager.GetString("cmd.tryout.list.tui", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Console.General] ConsoleMode is already "tui" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC after changing it for the new mode to take effect.. + /// + internal static string cmd_tryout_tui_already_enabled { + get { + return ResourceManager.GetString("cmd.tryout.tui.already_enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated [Console.General] ConsoleMode from "{0}" to "{1}" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC to apply the change.. + /// + internal static string cmd_tryout_tui_enabled { + get { + return ResourceManager.GetString("cmd.tryout.tui.enabled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display Health and Food saturation.. /// @@ -4254,6 +4407,87 @@ namespace MinecraftClient { return ResourceManager.GetString("cmd.nameitem.successful", resourceCulture); } } + + /// + /// Looks up a localized string similar to Failed to send recipe book craft request for {0}.. + /// + internal static string cmd_recipebook_craft_failed { + get { + return ResourceManager.GetString("cmd.recipebook.craft.failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Requested recipe {0}.. + /// + internal static string cmd_recipebook_craft_sent { + get { + return ResourceManager.GetString("cmd.recipebook.craft.sent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Requested recipe {0} with craft-all.. + /// + internal static string cmd_recipebook_craftall_sent { + get { + return ResourceManager.GetString("cmd.recipebook.craftall.sent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to List unlocked recipe book recipes and craft them through the active recipe book inventory.. + /// + internal static string cmd_recipebook_desc { + get { + return ResourceManager.GetString("cmd.recipebook.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unlocked recipe book recipes. + /// + internal static string cmd_recipebook_list { + get { + return ResourceManager.GetString("cmd.recipebook.list", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.. + /// + internal static string cmd_recipebook_no_active_inventory { + get { + return ResourceManager.GetString("cmd.recipebook.no.active.inventory", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No unlocked recipe book recipes are currently tracked.. + /// + internal static string cmd_recipebook_no_recipes { + get { + return ResourceManager.GetString("cmd.recipebook.no.recipes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The recipe identifier cannot be empty.. + /// + internal static string cmd_recipebook_recipe_id_empty { + get { + return ResourceManager.GetString("cmd.recipebook.recipe.id.empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Recipe book crafting is only supported on Minecraft 1.13 and newer.. + /// + internal static string cmd_recipebook_unsupported { + get { + return ResourceManager.GetString("cmd.recipebook.unsupported", resourceCulture); + } + } /// /// Looks up a localized string similar to restart and reconnect to the server.. @@ -4462,6 +4696,51 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to List all scoreboard teams and their members. + /// + internal static string cmd_teams_desc { + get { + return ResourceManager.GetString("cmd.teams.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No teams are currently tracked. + /// + internal static string cmd_teams_no_teams { + get { + return ResourceManager.GetString("cmd.teams.no_teams", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Team '{0}' (display: {1}, ...). + /// + internal static string cmd_teams_team_header { + get { + return ResourceManager.GetString("cmd.teams.team_header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Members ({0}): {1}. + /// + internal static string cmd_teams_team_members { + get { + return ResourceManager.GetString("cmd.teams.team_members", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No members. + /// + internal static string cmd_teams_team_no_members { + get { + return ResourceManager.GetString("cmd.teams.team_no_members", resourceCulture); + } + } + /// /// Looks up a localized string similar to Place a block or open chest. /// @@ -5534,6 +5813,42 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Tip: try TUI mode for a cleaner interface, mouse-friendly container actions, and a nicer layout. Run {0}feature tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart.. + /// + internal static string mcc_console_mode_tui_recommendation { + get { + return ResourceManager.GetString("mcc.console_mode_tui_recommendation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to MCC encountered a problem while starting TUI mode.. + /// + internal static string mcc_tui_startup_failed { + get { + return ResourceManager.GetString("mcc.tui_startup_failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC.. + /// + internal static string mcc_tui_startup_fallback_classic { + get { + return ResourceManager.GetString("mcc.tui_startup_fallback_classic", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please report this issue to the MCC Team.. + /// + internal static string mcc_report_issue { + get { + return ResourceManager.GetString("mcc.report_issue", resourceCulture); + } + } + /// /// Looks up a localized string similar to To sign in, open {0} in your browser and enter the code: {1}. /// @@ -5861,6 +6176,15 @@ namespace MinecraftClient { } } + /// + /// Looks up a localized string similar to Cannot send text: not connected to a server.. + /// + internal static string mcc_send_text_not_connected { + get { + return ResourceManager.GetString("mcc.send_text_not_connected", resourceCulture); + } + } + /// /// Looks up a localized string similar to Waiting {0} seconds before restarting.... /// @@ -6799,7 +7123,7 @@ namespace MinecraftClient { return ResourceManager.GetString("tui.crafting.grid", resourceCulture); } } - + /// /// Looks up a localized string similar to Starting embedded MCP server.... /// @@ -6808,7 +7132,7 @@ namespace MinecraftClient { return ResourceManager.GetString("bot.mcpserver.starting", resourceCulture); } } - + /// /// Looks up a localized string similar to Embedded MCP server started on {0}. /// @@ -6817,7 +7141,7 @@ namespace MinecraftClient { return ResourceManager.GetString("bot.mcpserver.started", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to start embedded MCP server: {0}. /// @@ -6826,7 +7150,7 @@ namespace MinecraftClient { return ResourceManager.GetString("bot.mcpserver.start_failed", resourceCulture); } } - + /// /// Looks up a localized string similar to Embedded MCP auth token is required but environment variable {0} is empty.. /// @@ -6835,7 +7159,7 @@ namespace MinecraftClient { return ResourceManager.GetString("bot.mcpserver.missing_auth_token", resourceCulture); } } - + /// /// Looks up a localized string similar to Embedded MCP server stopped.. /// @@ -6844,7 +7168,7 @@ namespace MinecraftClient { return ResourceManager.GetString("bot.mcpserver.stopped", resourceCulture); } } - + /// /// Looks up a localized string similar to Failed to stop embedded MCP server cleanly: {0}. /// @@ -6853,5 +7177,275 @@ namespace MinecraftClient { return ResourceManager.GetString("bot.mcpserver.stop_failed", resourceCulture); } } + + /// + /// Looks up a localized string similar to Toggle the TUI minimap overlay, or adjust its zoom level.. + /// + internal static string cmd_minimap_desc { + get { + return ResourceManager.GetString("cmd.minimap.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap enabled.. + /// + internal static string cmd_minimap_enabled { + get { + return ResourceManager.GetString("cmd.minimap.enabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap disabled.. + /// + internal static string cmd_minimap_disabled { + get { + return ResourceManager.GetString("cmd.minimap.disabled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap zoom set to {0}:1 (blocks per pixel).. + /// + internal static string cmd_minimap_zoom_set { + get { + return ResourceManager.GetString("cmd.minimap.zoom_set", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current minimap zoom: {0}:1 blocks/px (range 1-{1}).. + /// + internal static string cmd_minimap_zoom_current { + get { + return ResourceManager.GetString("cmd.minimap.zoom_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimap command is only available in TUI mode.. + /// + internal static string cmd_minimap_tui_only { + get { + return ResourceManager.GetString("cmd.minimap.tui_only", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hostile. + /// + internal static string tui_minimap_legend_hostile { + get { + return ResourceManager.GetString("tui.minimap.legend.hostile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passive. + /// + internal static string tui_minimap_legend_passive { + get { + return ResourceManager.GetString("tui.minimap.legend.passive", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Neutral. + /// + internal static string tui_minimap_legend_neutral { + get { + return ResourceManager.GetString("tui.minimap.legend.neutral", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Player. + /// + internal static string tui_minimap_legend_player { + get { + return ResourceManager.GetString("tui.minimap.legend.player", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}. + /// + internal static string cmd_minimap_names_status { + get { + return ResourceManager.GetString("cmd.minimap.names_status", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entity name labels enabled.. + /// + internal static string cmd_minimap_names_all_on { + get { + return ResourceManager.GetString("cmd.minimap.names_all_on", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to All entity name labels disabled.. + /// + internal static string cmd_minimap_names_all_off { + get { + return ResourceManager.GetString("cmd.minimap.names_all_off", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} name display: {1}. + /// + internal static string cmd_minimap_names_cat { + get { + return ResourceManager.GetString("cmd.minimap.names_cat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} name display set to {1}.. + /// + internal static string cmd_minimap_names_cat_set { + get { + return ResourceManager.GetString("cmd.minimap.names_cat_set", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current minimap position: {0}. + /// + internal static string cmd_minimap_position_current { + get { + return ResourceManager.GetString("cmd.minimap.position_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minimap position set to: {0}. + /// + internal static string cmd_minimap_position_set { + get { + return ResourceManager.GetString("cmd.minimap.position_set", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Current cave mode: {0}. + /// + internal static string cmd_minimap_cave_current { + get { + return ResourceManager.GetString("cmd.minimap.cave_current", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cave mode set to: {0}. + /// + internal static string cmd_minimap_cave_set { + get { + return ResourceManager.GetString("cmd.minimap.cave_set", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to list achievements/advancements from the server.. + /// + internal static string cmd_achievement_desc { + get { + return ResourceManager.GetString("cmd.achievement.desc", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No achievements/advancements received yet.. + /// + internal static string cmd_achievement_none { + get { + return ResourceManager.GetString("cmd.achievement.none", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No completed achievements/advancements.. + /// + internal static string cmd_achievement_none_unlocked { + get { + return ResourceManager.GetString("cmd.achievement.none_unlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No incomplete achievements/advancements.. + /// + internal static string cmd_achievement_none_locked { + get { + return ResourceManager.GetString("cmd.achievement.none_locked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Achievements/Advancements:. + /// + internal static string cmd_achievement_header { + get { + return ResourceManager.GetString("cmd.achievement.header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Completed achievements/advancements:. + /// + internal static string cmd_achievement_header_unlocked { + get { + return ResourceManager.GetString("cmd.achievement.header_unlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Incomplete achievements/advancements:. + /// + internal static string cmd_achievement_header_locked { + get { + return ResourceManager.GetString("cmd.achievement.header_locked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [DONE]. + /// + internal static string cmd_achievement_done { + get { + return ResourceManager.GetString("cmd.achievement.done", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [TODO]. + /// + internal static string cmd_achievement_todo { + get { + return ResourceManager.GetString("cmd.achievement.todo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} ({2}) [{3}]. + /// + internal static string cmd_achievement_entry_titled { + get { + return ResourceManager.GetString("cmd.achievement.entry_titled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} {1} [{2}]. + /// + internal static string cmd_achievement_entry { + get { + return ResourceManager.GetString("cmd.achievement.entry", resourceCulture); + } + } } } diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx index 33e965af..4807cc65 100644 --- a/MinecraftClient/Resources/Translations/Translations.resx +++ b/MinecraftClient/Resources/Translations/Translations.resx @@ -243,6 +243,9 @@ Inventory handling is not enabled. Unable to switch tools automatically. + + Dropped low durability {0} from slot {1}. + Automatic digging has started. @@ -252,6 +255,9 @@ Auto-digging has been stopped. + + Switch to {0} from slot {1}. + Added item {0} @@ -388,6 +394,12 @@ Waiting {0:0.000} seconds before reconnecting... + + Waiting {0:0.000} seconds before reconnecting... ({1} retries left) + + + unlimited + File not found: '{0}' @@ -830,6 +842,42 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file TestBot + + Minecraft Console Client v{0} - for MC {1} to {2} - {3} + + + Supported MC Versions: + + + Server: + + + Version: + + + Protocol: {0} + + + Players: + + + Ping: + + + {0} ms + + + Connecting as: + + + Online Players: + + + ... +{0} + + + Server reported protocol {0} ({1}), upgraded to {2} ({3}) for best compatibility + Converting session cache from disk: {0} @@ -1249,6 +1297,21 @@ Change EnableEmoji=false in the settings if the display is confusing. No active effects. + + try a recommended feature. + + + Available tryouts: + + + tui: set [Console.General] ConsoleMode = "tui" for the next restart. + + + [Console.General] ConsoleMode is already "tui" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC after changing it for the new mode to take effect. + + + Updated [Console.General] ConsoleMode from "{0}" to "{1}" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC to apply the change. + Display Health and Food saturation. @@ -1511,6 +1574,21 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Display server current tps (tick per second). May not be accurate + + List all scoreboard teams and their members. + + + No teams are currently tracked. + + + Team '{0}' (display: {1}, color: {2}, prefix: '{3}', suffix: '{4}', nameTagVisibility: {5}, collisionRule: {6}, friendlyFire: {7}, seeInvisibles: {8}) + + + Members ({0}): {1} + + + No members. + Place a block or open chest @@ -1863,6 +1941,18 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s Connecting to {0}... + + Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart. + + + MCC encountered a problem while starting TUI mode. + + + As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC. + + + Please report this issue to the MCC Team. + To sign in, open {0} in your browser and enter the code: §e{1} @@ -1974,6 +2064,9 @@ Type '{0}quit' to leave the server. Restarting Minecraft Console Client... + + Cannot send text: not connected to a server. + Waiting {0} seconds before restarting... @@ -1988,10 +2081,10 @@ MCC is running with default settings. Server is in offline mode. - Server version : {0} (protocol v{1}) + Server version: {0} (protocol v{1}) - Server version : + Server version: Checking Session... @@ -2151,6 +2244,33 @@ Logging in... Set an item name when an Anvil inventory is active and the item is in the first slot. + + Failed to send recipe book craft request for {0}. + + + Requested recipe {0}. + + + Requested recipe {0} with craft-all. + + + List unlocked recipe book recipes and craft them through the active recipe book inventory. + + + Unlocked recipe book recipes + + + You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory. + + + No unlocked recipe book recipes are currently tracked. + + + The recipe identifier cannot be empty. + + + Recipe book crafting is only supported on Minecraft 1.13 and newer. + Bot movement lock is held by bot {0}, so the Anti AFK bot might not move! @@ -2413,4 +2533,94 @@ see item details. Failed to stop embedded MCP server cleanly: {0} + + Toggle the TUI minimap overlay, or adjust its zoom level. + + + Minimap enabled. + + + Minimap disabled. + + + Minimap zoom set to {0}:1 (blocks per pixel). + + + Current minimap zoom: {0}:1 blocks/px (range 1-{1}). + + + The minimap command is only available in TUI mode. + + + Hostile + + + Passive + + + Neutral + + + Player + + + Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3} + + + All entity name labels enabled. + + + All entity name labels disabled. + + + {0} name display: {1} + + + {0} name display set to {1}. + + + Current minimap position: {0} + + + Minimap position set to: {0} + + + Current cave mode: {0} + + + Cave mode set to: {0} + + + list achievements/advancements from the server. + + + No achievements/advancements received yet. + + + No completed achievements/advancements. + + + No incomplete achievements/advancements. + + + Achievements/Advancements: + + + Completed achievements/advancements: + + + Incomplete achievements/advancements: + + + [DONE] + + + [TODO] + + + {0} {1} ({2}) [{3}] + + + {0} {1} [{2}] + diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 3c3c6271..f639edfa 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -205,6 +205,29 @@ namespace MinecraftClient.Scripting /// Entity with updated location public virtual void OnEntityMove(Entity entity) { } + /// + /// Called when a tracked entity receives a velocity update packet. + /// Velocity is expressed in blocks per tick. + /// + /// Entity with updated velocity + /// Velocity on X axis (blocks/tick) + /// Velocity on Y axis (blocks/tick) + /// Velocity on Z axis (blocks/tick) + public virtual void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) { } + + /// + /// Called when a sound packet is received. + /// The sound name is null when the protocol provides only a registry id. + /// + /// Sound key when available, otherwise null + /// Sound position when available + /// Sound category id from packet + /// Sound volume + /// Sound pitch + /// Source entity for entity-sound packets when tracked + public virtual void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, + Entity? sourceEntity) { } + /// /// Called when an entity rotates /// @@ -367,6 +390,23 @@ namespace MinecraftClient.Scripting /// Number format: 0 - blank, 1 - styled, 2 - fixed public virtual void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int value, int numberFormat) { } + /// + /// Called when a Teams packet is received from the server. + /// + /// Internal team name (up to 16 chars) + /// 0=create, 1=remove, 2=update, 3=add players, 4=remove players + /// Display name (formatted). Present when method is 0 or 2. + /// Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2. + /// Nametag visibility rule. Present when method is 0 or 2. + /// Collision rule. Present when method is 0 or 2. + /// ChatFormatting color value (-1=none). Present when method is 0 or 2. + /// Member name prefix (formatted). Present when method is 0 or 2. + /// Member name suffix (formatted). Present when method is 0 or 2. + /// Player/entity names. Present when method is 0, 3, or 4. + public virtual void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags, + string nameTagVisibility, string collisionRule, int color, + string prefix, string suffix, List players) { } + /// /// Called when the client received the Tab Header and Footer /// @@ -520,6 +560,14 @@ namespace MinecraftClient.Scripting /// The block public virtual void OnBlockChange(Location location, Block block) { } + /// + /// Called when achievement/advancement data is updated. + /// + /// Achievements that were added or updated + /// IDs of achievements that were removed + /// Whether the achievement state was fully reset before this update + public virtual void OnAchievementUpdate(IReadOnlyList updated, IReadOnlyList removedIds, bool reset) { } + /* =================================================================== */ /* ToolBox - Methods below might be useful while creating your bot. */ /* You should not need to interact with other classes of the program. */ @@ -1095,9 +1143,10 @@ namespace MinecraftClient.Scripting /// Example: if your player is under a block that is being destroyed, use Down /// Also perform the "arm swing" animation /// Also look at the block before digging - protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true) + /// Dig duration in seconds. 0 = auto-compute for survival, or instant for creative + protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true, double duration = 0) { - return Handler.DigBlock(location, direction, swingArms, lookAtBlock); + return Handler.DigBlock(location, direction, swingArms, lookAtBlock, duration); } /// @@ -1126,6 +1175,33 @@ namespace MinecraftClient.Scripting return Handler.GetEntities(); } + /// + /// Get all achievements/advancements. + /// + /// Snapshot of all achievements + protected Achievement[] GetAchievements() + { + return Handler.GetAchievements(); + } + + /// + /// Get only completed achievements/advancements. + /// + /// Snapshot of unlocked achievements + protected Achievement[] GetUnlockedAchievements() + { + return Handler.GetUnlockedAchievements(); + } + + /// + /// Get only incomplete achievements/advancements. + /// + /// Snapshot of locked achievements + protected Achievement[] GetLockedAchievements() + { + return Handler.GetLockedAchievements(); + } + /// /// Get all players Latency /// diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index b3fc1e1b..ffdea953 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1108,6 +1108,9 @@ namespace MinecraftClient [TomlPrecedingComment("$Console.CommandSuggestion$")] public CommandSuggestionConfig CommandSuggestion = new(); + [TomlPrecedingComment("$Console.Minimap$")] + public MinimapConfig Minimap = new(); + public void OnSettingUpdate() { var backend = ConsoleIO.Backend; @@ -1207,11 +1210,17 @@ namespace MinecraftClient [TomlInlineComment("$Console.General.ConsoleColorMode$")] public ConsoleColorModeType ConsoleColorMode = ConsoleColorModeType.vt100_24bit; + [TomlInlineComment("$Console.General.Display_Icon_Banner$")] + public bool Display_Icon_Banner = true; + [TomlInlineComment("$Console.General.Display_Input$")] public bool Display_Input = true; [TomlInlineComment("$Console.General.History_Input_Records$")] public int History_Input_Records = 32; + + [TomlInlineComment("$Console.General.TUI_Log_Scrollback$")] + public int TUI_Log_Scrollback = 0; } [TomlDoNotInlineObject] @@ -1246,6 +1255,53 @@ namespace MinecraftClient public enum ConsoleModeType { classic, tui }; public enum ConsoleColorModeType { disable, legacy_4bit, vt100_4bit, vt100_8bit, vt100_24bit }; + + [TomlDoNotInlineObject] + public class MinimapConfig + { + [TomlInlineComment("$Console.Minimap.Enabled$")] + public bool Enabled = false; + + [TomlInlineComment("$Console.Minimap.Zoom$")] + public int Zoom = Tui.MinimapControl.DefaultZoom; + + [TomlInlineComment("$Console.Minimap.Width$")] + public int Width = Tui.MinimapControl.DefaultWidth; + + [TomlInlineComment("$Console.Minimap.Height$")] + public int Height = Tui.MinimapControl.DefaultHeight; + + [TomlInlineComment("$Console.Minimap.Position$")] + public Tui.MinimapPosition Position = Tui.MinimapPosition.top_right; + + [TomlInlineComment("$Console.Minimap.ShowPlayerNames$")] + public bool ShowPlayerNames = false; + + [TomlInlineComment("$Console.Minimap.ShowHostileNames$")] + public bool ShowHostileNames = false; + + [TomlInlineComment("$Console.Minimap.ShowNeutralNames$")] + public bool ShowNeutralNames = false; + + [TomlInlineComment("$Console.Minimap.ShowPassiveNames$")] + public bool ShowPassiveNames = false; + + [TomlInlineComment("$Console.Minimap.RefreshInterval$")] + public int RefreshInterval = Tui.MinimapControl.DefaultRefreshMs; + + [TomlInlineComment("$Console.Minimap.CaveMode$")] + public Tui.CaveModeOption CaveMode = Tui.CaveModeOption.auto; + + public void OnSettingUpdate() + { + Zoom = Math.Clamp(Zoom, Tui.MinimapControl.MinZoom, Tui.MinimapControl.MaxZoom); + Width = Math.Clamp(Width, 10, 120); + Height = Math.Clamp(Height, 4, 80); + if (Height % 2 != 0) Height++; + RefreshInterval = Math.Clamp(RefreshInterval, + Tui.MinimapControl.MinRefreshMs, Tui.MinimapControl.MaxRefreshMs); + } + } } } diff --git a/MinecraftClient/Tui/IconGridBuilder.cs b/MinecraftClient/Tui/IconGridBuilder.cs new file mode 100644 index 00000000..3d13a8e2 --- /dev/null +++ b/MinecraftClient/Tui/IconGridBuilder.cs @@ -0,0 +1,125 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + internal static class IconGridBuilder + { + internal static Grid BuildFromRgba(byte[] rgba, int srcWidth, int srcHeight, int displaySize) + { + int cellCols = displaySize; + int cellRows = displaySize / 2; + + var grid = new Grid(); + for (int c = 0; c < cellCols; c++) + grid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto)); + for (int r = 0; r < cellRows; r++) + grid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto)); + + for (int row = 0; row < cellRows; row++) + { + for (int col = 0; col < cellCols; col++) + { + int topPixelY = row * 2; + int bottomPixelY = row * 2 + 1; + + var topColor = SamplePixel(rgba, srcWidth, srcHeight, col, topPixelY, cellCols, displaySize); + var bottomColor = SamplePixel(rgba, srcWidth, srcHeight, col, bottomPixelY, cellCols, displaySize); + + var cell = new TextBlock + { + Text = "\u2580", + Foreground = new SolidColorBrush(topColor), + Background = new SolidColorBrush(bottomColor), + Padding = new Thickness(0), + Margin = new Thickness(0), + }; + + Grid.SetRow(cell, row); + Grid.SetColumn(cell, col); + grid.Children.Add(cell); + } + } + + return grid; + } + + internal static Grid BuildFromBase64(string base64Data, int displaySize) + { + byte[] imageBytes; + try + { + imageBytes = Convert.FromBase64String(base64Data); + } + catch + { + return new Grid(); + } + + return BuildFromImageBytes(imageBytes, displaySize) ?? new Grid(); + } + + internal static Grid? BuildFromImageBytes(byte[] imageBytes, int displaySize) + { + int srcWidth, srcHeight; + byte[] rgba; + try + { + (srcWidth, srcHeight, rgba) = DecodeImageToRgba(imageBytes); + } + catch + { + return null; + } + + return BuildFromRgba(rgba, srcWidth, srcHeight, displaySize); + } + + internal static (int Width, int Height, byte[] Rgba) DecodeImageToRgba(byte[] imageData) + { + using var image = new ImageMagick.MagickImage(imageData); + int w = (int)image.Width; + int h = (int)image.Height; + + using var pixels = image.GetPixelsUnsafe(); + var rgba = new byte[w * h * 4]; + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var pixel = pixels.GetPixel(x, y)!; + int idx = (y * w + x) * 4; + var color = pixel.ToColor()!; + rgba[idx] = (byte)(color.R >> 8); + rgba[idx + 1] = (byte)(color.G >> 8); + rgba[idx + 2] = (byte)(color.B >> 8); + rgba[idx + 3] = (byte)(color.A >> 8); + } + } + + return (w, h, rgba); + } + + private static Color SamplePixel(byte[] rgba, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH) + { + int srcX = dstX * srcW / dstW; + int srcY = dstY * srcH / dstH; + srcX = Math.Clamp(srcX, 0, srcW - 1); + srcY = Math.Clamp(srcY, 0, srcH - 1); + + int idx = (srcY * srcW + srcX) * 4; + if (idx + 3 >= rgba.Length) + return Color.FromRgb(0, 0, 0); + + byte r = rgba[idx]; + byte g = rgba[idx + 1]; + byte b = rgba[idx + 2]; + byte a = rgba[idx + 3]; + + return a < 128 ? Color.FromRgb(0, 0, 0) : Color.FromRgb(r, g, b); + } + } +} diff --git a/MinecraftClient/Tui/MainTuiView.cs b/MinecraftClient/Tui/MainTuiView.cs index c2d51220..9affb94c 100644 --- a/MinecraftClient/Tui/MainTuiView.cs +++ b/MinecraftClient/Tui/MainTuiView.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -16,9 +17,20 @@ namespace MinecraftClient.Tui { public class MainTuiView : UserControl { - private const int MaxLogLines = 5000; + private static readonly int MaxLogLines = ResolveMaxLogLines(); private const int CtrlCDoublePressMsec = 1500; + private static int ResolveMaxLogLines() + { + int configured = Settings.Config.Console.General.TUI_Log_Scrollback; + if (configured > 0) + return configured; + + bool isArm = RuntimeInformation.ProcessArchitecture + is Architecture.Arm or Architecture.Arm64; + return isArm ? 500 : 3000; + } + private readonly ObservableCollection _logLines = new(); private readonly ObservableCollection _logControls = new(); private readonly ItemsControl _logItemsControl; @@ -41,6 +53,12 @@ namespace MinecraftClient.Tui private long _lastLogClickTicks; private const int DoubleClickMsec = 500; + private readonly Border _minimapBorder; + private readonly MinimapControl _minimapControl; + private volatile bool _minimapVisible; + + private TuiTooltipService? _tooltipService; + private readonly Border _suggestionBorder; private readonly StackPanel _suggestionPanel; private CommandSuggestion[] _suggestions = Array.Empty(); @@ -53,6 +71,8 @@ namespace MinecraftClient.Tui private int MaxVisibleSuggestions => Math.Max(1, Settings.Config.Console.CommandSuggestion.Max_Displayed_Suggestions); + public TuiTooltipService? TooltipService => _tooltipService; + public MainTuiView() { Background = Brushes.Black; @@ -70,6 +90,7 @@ namespace MinecraftClient.Tui { ItemsSource = _logControls, Focusable = false, + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel()), }; _logScrollViewer = new ScrollViewer @@ -150,6 +171,30 @@ namespace MinecraftClient.Tui Margin = new Thickness(0, 0, 0, 1), }; + var mmCfg = Settings.Config.Console.Minimap; + mmCfg.OnSettingUpdate(); + _minimapControl = new MinimapControl(mmCfg.Width, mmCfg.Height); + _minimapControl.BlocksPerPixel = mmCfg.Zoom; + _minimapControl.RefreshIntervalMs = mmCfg.RefreshInterval; + _minimapControl.NameConfig.Players = mmCfg.ShowPlayerNames; + _minimapControl.NameConfig.Hostile = mmCfg.ShowHostileNames; + _minimapControl.NameConfig.Neutral = mmCfg.ShowNeutralNames; + _minimapControl.NameConfig.Passive = mmCfg.ShowPassiveNames; + _minimapControl.CaveMode = mmCfg.CaveMode; + + var (hAlign, vAlign, margin) = GetMinimapAlignment(mmCfg.Position); + _minimapBorder = new Border + { + Background = new SolidColorBrush(Color.FromArgb(220, 15, 15, 15)), + BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)), + BorderThickness = new Thickness(1), + Child = _minimapControl, + IsVisible = false, + HorizontalAlignment = hAlign, + VerticalAlignment = vAlign, + Margin = margin, + }; + _mainContent = new DockPanel { Background = Brushes.Black, @@ -164,11 +209,22 @@ namespace MinecraftClient.Tui _rootPanel = new Panel { Background = Brushes.Black, - Children = { _mainContent, _notificationBorder, _suggestionBorder } + Children = { _mainContent, _minimapBorder, _notificationBorder, _suggestionBorder } }; + _tooltipService = new TuiTooltipService(_rootPanel); + _minimapControl.TooltipService = _tooltipService; + _minimapControl.Position = mmCfg.Position; + Content = _rootPanel; + if (mmCfg.Enabled) + { + _minimapVisible = true; + _minimapBorder.IsVisible = true; + _minimapControl.Start(); + } + StartStatusBarTimer(); } @@ -986,6 +1042,104 @@ namespace MinecraftClient.Tui #endregion + #region Minimap + + public void ShowMinimap() + { + if (_minimapVisible) return; + _minimapVisible = true; + _minimapBorder.IsVisible = true; + _minimapControl.Start(); + Settings.Config.Console.Minimap.Enabled = true; + } + + public void HideMinimap() + { + if (!_minimapVisible) return; + _minimapVisible = false; + _minimapControl.Stop(); + _minimapBorder.IsVisible = false; + Settings.Config.Console.Minimap.Enabled = false; + } + + public void ToggleMinimap() + { + if (_minimapVisible) + HideMinimap(); + else + ShowMinimap(); + } + + public bool IsMinimapVisible => _minimapVisible; + + public void SetMinimapZoom(int level) + { + _minimapControl.BlocksPerPixel = level; + Settings.Config.Console.Minimap.Zoom = level; + } + + public int GetMinimapZoom() => _minimapControl.BlocksPerPixel; + + public NameDisplayConfig GetMinimapNameConfig() => _minimapControl.NameConfig; + + public void SyncMinimapNameConfig() + { + var nc = _minimapControl.NameConfig; + var cfg = Settings.Config.Console.Minimap; + cfg.ShowPlayerNames = nc.Players; + cfg.ShowHostileNames = nc.Hostile; + cfg.ShowNeutralNames = nc.Neutral; + cfg.ShowPassiveNames = nc.Passive; + } + + public void ResizeMinimap(int width, int height) + { + _minimapControl.Resize(width, height); + Settings.Config.Console.Minimap.Width = width; + Settings.Config.Console.Minimap.Height = height; + } + + public void SetMinimapPosition(MinimapPosition pos) + { + var (hAlign, vAlign, margin) = GetMinimapAlignment(pos); + _minimapBorder.HorizontalAlignment = hAlign; + _minimapBorder.VerticalAlignment = vAlign; + _minimapBorder.Margin = margin; + _minimapControl.Position = pos; + Settings.Config.Console.Minimap.Position = pos; + } + + public MinimapPosition GetMinimapPosition() => Settings.Config.Console.Minimap.Position; + + public void SetMinimapCaveMode(CaveModeOption mode) + { + _minimapControl.CaveMode = mode; + Settings.Config.Console.Minimap.CaveMode = mode; + } + + public CaveModeOption GetMinimapCaveMode() => _minimapControl.CaveMode; + + private static (HorizontalAlignment h, VerticalAlignment v, Thickness margin) GetMinimapAlignment(MinimapPosition pos) => pos switch + { + MinimapPosition.top_left => (HorizontalAlignment.Left, VerticalAlignment.Top, new Thickness(1, 1, 0, 0)), + MinimapPosition.top_right => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)), + MinimapPosition.center => (HorizontalAlignment.Center, VerticalAlignment.Center, new Thickness(0)), + MinimapPosition.bottom_left => (HorizontalAlignment.Left, VerticalAlignment.Bottom, new Thickness(1, 0, 0, 2)), + MinimapPosition.bottom_right => (HorizontalAlignment.Right, VerticalAlignment.Bottom, new Thickness(0, 0, 1, 2)), + _ => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)), + }; + + public void ApplyMinimapConfig() + { + var cfg = Settings.Config.Console.Minimap; + if (cfg.Enabled && !_minimapVisible) + ShowMinimap(); + else if (!cfg.Enabled && _minimapVisible) + HideMinimap(); + } + + #endregion + #region Overlay public void ShowOverlay(Control content, Action? onClose = null) @@ -1039,5 +1193,18 @@ namespace MinecraftClient.Tui _commandInput.Focus(); }, DispatcherPriority.Loaded); } + + #region Custom Control Append + + public void AppendControlToLog(Control control) + { + _logLines.Add(string.Empty); + _logControls.Add(control); + TrimLog(); + if (_autoScroll) + ScheduleScrollToEnd(); + } + + #endregion } } diff --git a/MinecraftClient/Tui/McColorParser.cs b/MinecraftClient/Tui/McColorParser.cs index c46b0adf..30f8f280 100644 --- a/MinecraftClient/Tui/McColorParser.cs +++ b/MinecraftClient/Tui/McColorParser.cs @@ -52,6 +52,8 @@ namespace MinecraftClient.Tui IBrush currentColor = Brushes.White; bool bold = false; bool italic = false; + bool underline = false; + bool strikethrough = false; int start = 0; for (int i = 0; i < text.Length; i++) @@ -59,7 +61,7 @@ namespace MinecraftClient.Tui if (text[i] == '§' && i + 1 < text.Length) { if (i > start) - AddRun(tb, text[start..i], currentColor, bold, italic); + AddRun(tb, text[start..i], currentColor, bold, italic, underline, strikethrough); char code = char.ToLower(text[i + 1]); @@ -68,6 +70,8 @@ namespace MinecraftClient.Tui currentColor = brush; bold = false; italic = false; + underline = false; + strikethrough = false; } else { @@ -75,10 +79,14 @@ namespace MinecraftClient.Tui { case 'l': bold = true; break; case 'o': italic = true; break; + case 'n': underline = true; break; + case 'm': strikethrough = true; break; case 'r': currentColor = Brushes.White; bold = false; italic = false; + underline = false; + strikethrough = false; break; } } @@ -89,7 +97,7 @@ namespace MinecraftClient.Tui } if (start < text.Length) - AddRun(tb, text[start..], currentColor, bold, italic); + AddRun(tb, text[start..], currentColor, bold, italic, underline, strikethrough); if (tb.Inlines?.Count == 0) { @@ -100,16 +108,29 @@ namespace MinecraftClient.Tui return tb; } - private static void AddRun(TextBlock tb, string text, IBrush color, bool bold, bool italic) + private static void AddRun(TextBlock tb, string text, IBrush color, + bool bold, bool italic, bool underline, bool strikethrough) { if (text.Length == 0) return; tb.Inlines ??= new InlineCollection(); + + TextDecorationCollection? decorations = null; + if (underline || strikethrough) + { + decorations = []; + if (underline) + decorations.Add(new TextDecoration { Location = TextDecorationLocation.Underline }); + if (strikethrough) + decorations.Add(new TextDecoration { Location = TextDecorationLocation.Strikethrough }); + } + tb.Inlines.Add(new Run(text) { Foreground = color, FontWeight = bold ? FontWeight.Bold : FontWeight.Normal, FontStyle = italic ? FontStyle.Italic : FontStyle.Normal, + TextDecorations = decorations, }); } } diff --git a/MinecraftClient/Tui/MccBannerPanelBuilder.cs b/MinecraftClient/Tui/MccBannerPanelBuilder.cs new file mode 100644 index 00000000..36d35b40 --- /dev/null +++ b/MinecraftClient/Tui/MccBannerPanelBuilder.cs @@ -0,0 +1,178 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Layout; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + internal static class MccBannerPanelBuilder + { + internal static Border Build(string? buildInfo) + { + var contentPanel = new DockPanel { Background = Brushes.Black }; + + var icon = BuildIcon(); + icon.VerticalAlignment = VerticalAlignment.Center; + DockPanel.SetDock(icon, Dock.Left); + contentPanel.Children.Add(icon); + + var infoPanel = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new Thickness(1, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + }; + + AddTitle(infoPanel); + AddVersionRange(infoPanel); + AddGithub(infoPanel); + + if (buildInfo is not null) + AddBuildInfo(infoPanel, buildInfo); + + contentPanel.Children.Add(infoPanel); + + return new Border + { + BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)), + BorderThickness = new Thickness(1), + Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)), + Padding = new Thickness(1, 0), + Child = contentPanel, + Margin = new Thickness(0), + }; + } + + private static void AddTitle(StackPanel panel) + { + var row = new TextBlock(); + row.Inlines!.Add(new Run("Minecraft Console Client") + { Foreground = Pal.Gold, FontWeight = FontWeight.Bold }); + row.Inlines.Add(new Run($" v{Program.Version}") { Foreground = Pal.Aqua }); + panel.Children.Add(row); + } + + private static void AddVersionRange(StackPanel panel) + { + var row = new TextBlock(); + row.Inlines!.Add(Lbl(Translations.mcc_banner_label_mc_versions)); + row.Inlines.Add(Val(Program.MCLowestVersion, Pal.Green)); + row.Inlines.Add(new Run(" - ") { Foreground = Pal.Gray }); + row.Inlines.Add(Val(Program.MCHighestVersion, Pal.Green)); + panel.Children.Add(row); + } + + private static void AddGithub(StackPanel panel) + { + var row = new TextBlock(); + row.Inlines!.Add(Val("Github.com/MCCTeam", Pal.Gray)); + panel.Children.Add(row); + } + + private static void AddBuildInfo(StackPanel panel, string buildInfo) + { + panel.Children.Add(new TextBlock + { + Text = buildInfo, + Foreground = Pal.DarkGray, + }); + } + + #region Icon + + private static readonly Color B1 = Color.FromRgb(200, 200, 200); // bezel bright + private static readonly Color B2 = Color.FromRgb(160, 160, 160); // bezel mid + private static readonly Color B3 = Color.FromRgb(120, 120, 120); // bezel dark + private static readonly Color S = Color.FromRgb(20, 20, 20); // screen bg + private static readonly Color C = Color.FromRgb(55, 200, 55); // creeper green + + // @formatter:off + private static readonly Color[,] Pixels = + { + { B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B2 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, C, C, S, S, C, C, S, B3 }, + { B1, S, S, S, S, S, S, S, S, C, C, S, S, C, C, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, C, C, S, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, C, S, S, C, S, S, B3 }, + { B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 }, + { B2, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3 }, + }; + // @formatter:on + + private static Control BuildIcon() + { + int cols = Pixels.GetLength(1); + int textRows = Pixels.GetLength(0) / 2; + + var pixelGrid = new Grid(); + for (int c = 0; c < cols; c++) + pixelGrid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto)); + for (int r = 0; r < textRows; r++) + pixelGrid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto)); + + for (int row = 0; row < textRows; row++) + { + for (int col = 0; col < cols; col++) + { + var topColor = Pixels[row * 2, col]; + var bottomColor = Pixels[row * 2 + 1, col]; + + var cell = new TextBlock + { + Text = "\u2580", + Foreground = new SolidColorBrush(topColor), + Background = new SolidColorBrush(bottomColor), + Padding = new Thickness(0), + Margin = new Thickness(0), + }; + + Grid.SetRow(cell, row); + Grid.SetColumn(cell, col); + pixelGrid.Children.Add(cell); + } + } + + var prompt = new TextBlock + { + Text = " >_", + Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255)), + Background = new SolidColorBrush(S), + Padding = new Thickness(0), + Margin = new Thickness(0), + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + }; + Grid.SetRow(prompt, 1); + Grid.SetColumn(prompt, 1); + Grid.SetColumnSpan(prompt, 4); + pixelGrid.Children.Add(prompt); + + return pixelGrid; + } + + #endregion + + private static Run Lbl(string text) => + new(text + " ") { Foreground = Pal.Gray }; + + private static Run Val(string text, IBrush color) => + new(text) { Foreground = color }; + + private static class Pal + { + public static readonly IBrush Gray = new SolidColorBrush(Color.FromRgb(170, 170, 170)); + public static readonly IBrush DarkGray = new SolidColorBrush(Color.FromRgb(85, 85, 85)); + public static readonly IBrush Aqua = new SolidColorBrush(Color.FromRgb(85, 255, 255)); + public static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(85, 255, 85)); + public static readonly IBrush Gold = new SolidColorBrush(Color.FromRgb(255, 170, 0)); + } + } +} diff --git a/MinecraftClient/Tui/MinimapBlockColors.json b/MinecraftClient/Tui/MinimapBlockColors.json new file mode 100644 index 00000000..af7d726c --- /dev/null +++ b/MinecraftClient/Tui/MinimapBlockColors.json @@ -0,0 +1,3552 @@ +{ + "version": "26.1-rc-2", + "colors": { + "AcaciaDoor": [ + 216, + 127, + 51 + ], + "AcaciaFence": [ + 216, + 127, + 51 + ], + "AcaciaFenceGate": [ + 216, + 127, + 51 + ], + "AcaciaHangingSign": [ + 216, + 127, + 51 + ], + "AcaciaPlanks": [ + 216, + 127, + 51 + ], + "AcaciaPressurePlate": [ + 216, + 127, + 51 + ], + "AcaciaSapling": [ + 0, + 124, + 0 + ], + "AcaciaShelf": [ + 216, + 127, + 51 + ], + "AcaciaSign": [ + 216, + 127, + 51 + ], + "AcaciaSlab": [ + 216, + 127, + 51 + ], + "AcaciaTrapdoor": [ + 216, + 127, + 51 + ], + "AcaciaWallHangingSign": [ + 216, + 127, + 51 + ], + "AcaciaWallSign": [ + 216, + 127, + 51 + ], + "AcaciaWood": [ + 76, + 76, + 76 + ], + "Allium": [ + 0, + 124, + 0 + ], + "AmethystBlock": [ + 127, + 63, + 178 + ], + "AmethystCluster": [ + 127, + 63, + 178 + ], + "AncientDebris": [ + 25, + 25, + 25 + ], + "Andesite": [ + 112, + 112, + 112 + ], + "Anvil": [ + 167, + 167, + 167 + ], + "AttachedMelonStem": [ + 0, + 124, + 0 + ], + "AttachedPumpkinStem": [ + 0, + 124, + 0 + ], + "Azalea": [ + 0, + 124, + 0 + ], + "AzureBluet": [ + 0, + 124, + 0 + ], + "Bamboo": [ + 0, + 124, + 0 + ], + "BambooDoor": [ + 229, + 229, + 51 + ], + "BambooFence": [ + 229, + 229, + 51 + ], + "BambooFenceGate": [ + 229, + 229, + 51 + ], + "BambooHangingSign": [ + 229, + 229, + 51 + ], + "BambooMosaic": [ + 229, + 229, + 51 + ], + "BambooMosaicSlab": [ + 229, + 229, + 51 + ], + "BambooPlanks": [ + 229, + 229, + 51 + ], + "BambooPressurePlate": [ + 229, + 229, + 51 + ], + "BambooSapling": [ + 143, + 119, + 72 + ], + "BambooShelf": [ + 229, + 229, + 51 + ], + "BambooSign": [ + 229, + 229, + 51 + ], + "BambooSlab": [ + 229, + 229, + 51 + ], + "BambooTrapdoor": [ + 229, + 229, + 51 + ], + "BambooWallHangingSign": [ + 229, + 229, + 51 + ], + "BambooWallSign": [ + 229, + 229, + 51 + ], + "Barrel": [ + 143, + 119, + 72 + ], + "Barrier": [ + 0, + 0, + 0 + ], + "Basalt": [ + 25, + 25, + 25 + ], + "Beacon": [ + 92, + 219, + 213 + ], + "Bedrock": [ + 112, + 112, + 112 + ], + "BeeNest": [ + 229, + 229, + 51 + ], + "Beehive": [ + 143, + 119, + 72 + ], + "Beetroots": [ + 0, + 124, + 0 + ], + "Bell": [ + 250, + 238, + 77 + ], + "BigDripleaf": [ + 0, + 124, + 0 + ], + "BigDripleafStem": [ + 0, + 124, + 0 + ], + "BirchDoor": [ + 247, + 233, + 163 + ], + "BirchFence": [ + 247, + 233, + 163 + ], + "BirchFenceGate": [ + 247, + 233, + 163 + ], + "BirchHangingSign": [ + 247, + 233, + 163 + ], + "BirchPlanks": [ + 247, + 233, + 163 + ], + "BirchPressurePlate": [ + 247, + 233, + 163 + ], + "BirchSapling": [ + 0, + 124, + 0 + ], + "BirchShelf": [ + 247, + 233, + 163 + ], + "BirchSign": [ + 247, + 233, + 163 + ], + "BirchSlab": [ + 247, + 233, + 163 + ], + "BirchTrapdoor": [ + 247, + 233, + 163 + ], + "BirchWallHangingSign": [ + 247, + 233, + 163 + ], + "BirchWallSign": [ + 247, + 233, + 163 + ], + "BirchWood": [ + 247, + 233, + 163 + ], + "BlackBanner": [ + 143, + 119, + 72 + ], + "BlackCarpet": [ + 25, + 25, + 25 + ], + "BlackConcrete": [ + 25, + 25, + 25 + ], + "BlackConcretePowder": [ + 25, + 25, + 25 + ], + "BlackGlazedTerracotta": [ + 25, + 25, + 25 + ], + "BlackTerracotta": [ + 37, + 22, + 16 + ], + "BlackWallBanner": [ + 143, + 119, + 72 + ], + "BlackWool": [ + 25, + 25, + 25 + ], + "Blackstone": [ + 25, + 25, + 25 + ], + "BlastFurnace": [ + 112, + 112, + 112 + ], + "BlueBanner": [ + 143, + 119, + 72 + ], + "BlueCarpet": [ + 51, + 76, + 178 + ], + "BlueConcrete": [ + 51, + 76, + 178 + ], + "BlueConcretePowder": [ + 51, + 76, + 178 + ], + "BlueGlazedTerracotta": [ + 51, + 76, + 178 + ], + "BlueIce": [ + 160, + 160, + 255 + ], + "BlueOrchid": [ + 0, + 124, + 0 + ], + "BlueTerracotta": [ + 76, + 62, + 92 + ], + "BlueWallBanner": [ + 143, + 119, + 72 + ], + "BlueWool": [ + 51, + 76, + 178 + ], + "BoneBlock": [ + 247, + 233, + 163 + ], + "Bookshelf": [ + 143, + 119, + 72 + ], + "BrainCoral": [ + 242, + 127, + 165 + ], + "BrainCoralBlock": [ + 242, + 127, + 165 + ], + "BrainCoralFan": [ + 242, + 127, + 165 + ], + "BrainCoralWallFan": [ + 242, + 127, + 165 + ], + "BrewingStand": [ + 167, + 167, + 167 + ], + "BrickSlab": [ + 153, + 51, + 51 + ], + "Bricks": [ + 153, + 51, + 51 + ], + "BrownBanner": [ + 143, + 119, + 72 + ], + "BrownCarpet": [ + 102, + 76, + 51 + ], + "BrownConcrete": [ + 102, + 76, + 51 + ], + "BrownConcretePowder": [ + 102, + 76, + 51 + ], + "BrownGlazedTerracotta": [ + 102, + 76, + 51 + ], + "BrownMushroom": [ + 102, + 76, + 51 + ], + "BrownMushroomBlock": [ + 151, + 109, + 77 + ], + "BrownTerracotta": [ + 76, + 50, + 35 + ], + "BrownWallBanner": [ + 143, + 119, + 72 + ], + "BrownWool": [ + 102, + 76, + 51 + ], + "BubbleColumn": [ + 64, + 64, + 255 + ], + "BubbleCoral": [ + 127, + 63, + 178 + ], + "BubbleCoralBlock": [ + 127, + 63, + 178 + ], + "BubbleCoralFan": [ + 127, + 63, + 178 + ], + "BubbleCoralWallFan": [ + 127, + 63, + 178 + ], + "BuddingAmethyst": [ + 127, + 63, + 178 + ], + "Bush": [ + 0, + 124, + 0 + ], + "Cactus": [ + 0, + 124, + 0 + ], + "CactusFlower": [ + 242, + 127, + 165 + ], + "Calcite": [ + 209, + 177, + 161 + ], + "Campfire": [ + 129, + 86, + 49 + ], + "Carrots": [ + 0, + 124, + 0 + ], + "CartographyTable": [ + 143, + 119, + 72 + ], + "CarvedPumpkin": [ + 216, + 127, + 51 + ], + "Cauldron": [ + 112, + 112, + 112 + ], + "CaveVines": [ + 0, + 124, + 0 + ], + "CaveVinesPlant": [ + 0, + 124, + 0 + ], + "ChainCommandBlock": [ + 102, + 127, + 51 + ], + "CherryDoor": [ + 209, + 177, + 161 + ], + "CherryFence": [ + 209, + 177, + 161 + ], + "CherryFenceGate": [ + 209, + 177, + 161 + ], + "CherryHangingSign": [ + 160, + 77, + 78 + ], + "CherryLeaves": [ + 242, + 127, + 165 + ], + "CherryPlanks": [ + 209, + 177, + 161 + ], + "CherryPressurePlate": [ + 209, + 177, + 161 + ], + "CherrySapling": [ + 242, + 127, + 165 + ], + "CherryShelf": [ + 209, + 177, + 161 + ], + "CherrySign": [ + 209, + 177, + 161 + ], + "CherrySlab": [ + 209, + 177, + 161 + ], + "CherryTrapdoor": [ + 209, + 177, + 161 + ], + "CherryWallHangingSign": [ + 160, + 77, + 78 + ], + "CherryWood": [ + 57, + 41, + 35 + ], + "Chest": [ + 143, + 119, + 72 + ], + "ChippedAnvil": [ + 167, + 167, + 167 + ], + "ChiseledBookshelf": [ + 143, + 119, + 72 + ], + "ChiseledNetherBricks": [ + 112, + 2, + 0 + ], + "ChiseledQuartzBlock": [ + 255, + 252, + 245 + ], + "ChiseledRedSandstone": [ + 216, + 127, + 51 + ], + "ChiseledResinBricks": [ + 159, + 82, + 36 + ], + "ChiseledSandstone": [ + 247, + 233, + 163 + ], + "ChiseledStoneBricks": [ + 112, + 112, + 112 + ], + "ChorusFlower": [ + 127, + 63, + 178 + ], + "ChorusPlant": [ + 127, + 63, + 178 + ], + "Clay": [ + 164, + 168, + 184 + ], + "ClosedEyeblossom": [ + 167, + 167, + 167 + ], + "CoalBlock": [ + 25, + 25, + 25 + ], + "CoalOre": [ + 112, + 112, + 112 + ], + "CoarseDirt": [ + 151, + 109, + 77 + ], + "Cobblestone": [ + 112, + 112, + 112 + ], + "CobblestoneSlab": [ + 112, + 112, + 112 + ], + "Cobweb": [ + 199, + 199, + 199 + ], + "Cocoa": [ + 0, + 124, + 0 + ], + "CommandBlock": [ + 102, + 76, + 51 + ], + "Composter": [ + 143, + 119, + 72 + ], + "Conduit": [ + 92, + 219, + 213 + ], + "CopperBlock": [ + 216, + 127, + 51 + ], + "CopperBulb": [ + 216, + 127, + 51 + ], + "CopperChest": [ + 216, + 127, + 51 + ], + "CopperDoor": [ + 216, + 127, + 51 + ], + "CopperGolemStatue": [ + 216, + 127, + 51 + ], + "CopperGrate": [ + 216, + 127, + 51 + ], + "CopperTrapdoor": [ + 216, + 127, + 51 + ], + "Cornflower": [ + 0, + 124, + 0 + ], + "CrackedNetherBricks": [ + 112, + 2, + 0 + ], + "CrackedStoneBricks": [ + 112, + 112, + 112 + ], + "Crafter": [ + 112, + 112, + 112 + ], + "CraftingTable": [ + 143, + 119, + 72 + ], + "CreakingHeart": [ + 216, + 127, + 51 + ], + "CrimsonDoor": [ + 148, + 63, + 97 + ], + "CrimsonFence": [ + 148, + 63, + 97 + ], + "CrimsonFenceGate": [ + 148, + 63, + 97 + ], + "CrimsonFungus": [ + 112, + 2, + 0 + ], + "CrimsonHangingSign": [ + 148, + 63, + 97 + ], + "CrimsonHyphae": [ + 92, + 25, + 29 + ], + "CrimsonNylium": [ + 189, + 48, + 49 + ], + "CrimsonPlanks": [ + 148, + 63, + 97 + ], + "CrimsonPressurePlate": [ + 148, + 63, + 97 + ], + "CrimsonRoots": [ + 112, + 2, + 0 + ], + "CrimsonShelf": [ + 148, + 63, + 97 + ], + "CrimsonSign": [ + 148, + 63, + 97 + ], + "CrimsonSlab": [ + 148, + 63, + 97 + ], + "CrimsonTrapdoor": [ + 148, + 63, + 97 + ], + "CrimsonWallHangingSign": [ + 148, + 63, + 97 + ], + "CrimsonWallSign": [ + 148, + 63, + 97 + ], + "CryingObsidian": [ + 25, + 25, + 25 + ], + "CutRedSandstone": [ + 216, + 127, + 51 + ], + "CutRedSandstoneSlab": [ + 216, + 127, + 51 + ], + "CutSandstone": [ + 247, + 233, + 163 + ], + "CutSandstoneSlab": [ + 247, + 233, + 163 + ], + "CyanBanner": [ + 143, + 119, + 72 + ], + "CyanCarpet": [ + 76, + 127, + 153 + ], + "CyanConcrete": [ + 76, + 127, + 153 + ], + "CyanConcretePowder": [ + 76, + 127, + 153 + ], + "CyanGlazedTerracotta": [ + 76, + 127, + 153 + ], + "CyanTerracotta": [ + 87, + 92, + 92 + ], + "CyanWallBanner": [ + 143, + 119, + 72 + ], + "CyanWool": [ + 76, + 127, + 153 + ], + "DamagedAnvil": [ + 167, + 167, + 167 + ], + "Dandelion": [ + 0, + 124, + 0 + ], + "DarkOakDoor": [ + 102, + 76, + 51 + ], + "DarkOakFence": [ + 102, + 76, + 51 + ], + "DarkOakFenceGate": [ + 102, + 76, + 51 + ], + "DarkOakPlanks": [ + 102, + 76, + 51 + ], + "DarkOakPressurePlate": [ + 102, + 76, + 51 + ], + "DarkOakSapling": [ + 0, + 124, + 0 + ], + "DarkOakSlab": [ + 102, + 76, + 51 + ], + "DarkOakTrapdoor": [ + 102, + 76, + 51 + ], + "DarkOakWood": [ + 102, + 76, + 51 + ], + "DarkPrismarine": [ + 92, + 219, + 213 + ], + "DarkPrismarineSlab": [ + 92, + 219, + 213 + ], + "DaylightDetector": [ + 143, + 119, + 72 + ], + "DeadBrainCoral": [ + 76, + 76, + 76 + ], + "DeadBrainCoralBlock": [ + 76, + 76, + 76 + ], + "DeadBrainCoralFan": [ + 76, + 76, + 76 + ], + "DeadBrainCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadBubbleCoral": [ + 76, + 76, + 76 + ], + "DeadBubbleCoralBlock": [ + 76, + 76, + 76 + ], + "DeadBubbleCoralFan": [ + 76, + 76, + 76 + ], + "DeadBubbleCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadBush": [ + 143, + 119, + 72 + ], + "DeadFireCoral": [ + 76, + 76, + 76 + ], + "DeadFireCoralBlock": [ + 76, + 76, + 76 + ], + "DeadFireCoralFan": [ + 76, + 76, + 76 + ], + "DeadFireCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadHornCoral": [ + 76, + 76, + 76 + ], + "DeadHornCoralBlock": [ + 76, + 76, + 76 + ], + "DeadHornCoralFan": [ + 76, + 76, + 76 + ], + "DeadHornCoralWallFan": [ + 76, + 76, + 76 + ], + "DeadTubeCoral": [ + 76, + 76, + 76 + ], + "DeadTubeCoralBlock": [ + 76, + 76, + 76 + ], + "DeadTubeCoralFan": [ + 76, + 76, + 76 + ], + "DeadTubeCoralWallFan": [ + 76, + 76, + 76 + ], + "DecoratedPot": [ + 142, + 60, + 46 + ], + "Deepslate": [ + 100, + 100, + 100 + ], + "DeepslateCoalOre": [ + 100, + 100, + 100 + ], + "DeepslateCopperOre": [ + 100, + 100, + 100 + ], + "DeepslateDiamondOre": [ + 100, + 100, + 100 + ], + "DeepslateEmeraldOre": [ + 100, + 100, + 100 + ], + "DeepslateGoldOre": [ + 100, + 100, + 100 + ], + "DeepslateIronOre": [ + 100, + 100, + 100 + ], + "DeepslateLapisOre": [ + 100, + 100, + 100 + ], + "DeepslateRedstoneOre": [ + 100, + 100, + 100 + ], + "DiamondBlock": [ + 92, + 219, + 213 + ], + "DiamondOre": [ + 112, + 112, + 112 + ], + "Diorite": [ + 255, + 252, + 245 + ], + "Dirt": [ + 151, + 109, + 77 + ], + "DirtPath": [ + 151, + 109, + 77 + ], + "Dispenser": [ + 112, + 112, + 112 + ], + "DragonEgg": [ + 25, + 25, + 25 + ], + "DriedGhast": [ + 76, + 76, + 76 + ], + "DriedKelpBlock": [ + 102, + 127, + 51 + ], + "DripstoneBlock": [ + 76, + 50, + 35 + ], + "Dropper": [ + 112, + 112, + 112 + ], + "EmeraldBlock": [ + 0, + 217, + 58 + ], + "EmeraldOre": [ + 112, + 112, + 112 + ], + "EnchantingTable": [ + 153, + 51, + 51 + ], + "EndGateway": [ + 25, + 25, + 25 + ], + "EndPortal": [ + 25, + 25, + 25 + ], + "EndPortalFrame": [ + 102, + 127, + 51 + ], + "EndStone": [ + 247, + 233, + 163 + ], + "EndStoneBricks": [ + 247, + 233, + 163 + ], + "EnderChest": [ + 112, + 112, + 112 + ], + "ExposedCopper": [ + 135, + 107, + 98 + ], + "ExposedCopperBulb": [ + 135, + 107, + 98 + ], + "ExposedCopperChest": [ + 135, + 107, + 98 + ], + "ExposedCopperDoor": [ + 135, + 107, + 98 + ], + "ExposedCopperGolemStatue": [ + 135, + 107, + 98 + ], + "ExposedCopperGrate": [ + 135, + 107, + 98 + ], + "ExposedCopperTrapdoor": [ + 135, + 107, + 98 + ], + "ExposedLightningRod": [ + 135, + 107, + 98 + ], + "Farmland": [ + 151, + 109, + 77 + ], + "Fern": [ + 0, + 124, + 0 + ], + "Fire": [ + 255, + 0, + 0 + ], + "FireCoral": [ + 153, + 51, + 51 + ], + "FireCoralBlock": [ + 153, + 51, + 51 + ], + "FireCoralFan": [ + 153, + 51, + 51 + ], + "FireCoralWallFan": [ + 153, + 51, + 51 + ], + "FireflyBush": [ + 0, + 124, + 0 + ], + "FletchingTable": [ + 143, + 119, + 72 + ], + "FloweringAzalea": [ + 0, + 124, + 0 + ], + "Frogspawn": [ + 64, + 64, + 255 + ], + "FrostedIce": [ + 160, + 160, + 255 + ], + "Furnace": [ + 112, + 112, + 112 + ], + "GlowLichen": [ + 127, + 167, + 150 + ], + "Glowstone": [ + 247, + 233, + 163 + ], + "GoldBlock": [ + 250, + 238, + 77 + ], + "GoldOre": [ + 112, + 112, + 112 + ], + "GoldenDandelion": [ + 0, + 124, + 0 + ], + "Granite": [ + 151, + 109, + 77 + ], + "GrassBlock": [ + 127, + 178, + 56 + ], + "Gravel": [ + 112, + 112, + 112 + ], + "GrayBanner": [ + 143, + 119, + 72 + ], + "GrayCarpet": [ + 76, + 76, + 76 + ], + "GrayConcrete": [ + 76, + 76, + 76 + ], + "GrayConcretePowder": [ + 76, + 76, + 76 + ], + "GrayGlazedTerracotta": [ + 76, + 76, + 76 + ], + "GrayTerracotta": [ + 57, + 41, + 35 + ], + "GrayWallBanner": [ + 143, + 119, + 72 + ], + "GrayWool": [ + 76, + 76, + 76 + ], + "GreenBanner": [ + 143, + 119, + 72 + ], + "GreenCarpet": [ + 102, + 127, + 51 + ], + "GreenConcrete": [ + 102, + 127, + 51 + ], + "GreenConcretePowder": [ + 102, + 127, + 51 + ], + "GreenGlazedTerracotta": [ + 102, + 127, + 51 + ], + "GreenTerracotta": [ + 76, + 82, + 42 + ], + "GreenWallBanner": [ + 143, + 119, + 72 + ], + "GreenWool": [ + 102, + 127, + 51 + ], + "Grindstone": [ + 167, + 167, + 167 + ], + "HangingRoots": [ + 151, + 109, + 77 + ], + "HayBlock": [ + 229, + 229, + 51 + ], + "HeavyCore": [ + 167, + 167, + 167 + ], + "HeavyWeightedPressurePlate": [ + 167, + 167, + 167 + ], + "HoneyBlock": [ + 216, + 127, + 51 + ], + "HoneycombBlock": [ + 216, + 127, + 51 + ], + "Hopper": [ + 112, + 112, + 112 + ], + "HornCoral": [ + 229, + 229, + 51 + ], + "HornCoralBlock": [ + 229, + 229, + 51 + ], + "HornCoralFan": [ + 229, + 229, + 51 + ], + "HornCoralWallFan": [ + 229, + 229, + 51 + ], + "Ice": [ + 160, + 160, + 255 + ], + "InfestedChiseledStoneBricks": [ + 164, + 168, + 184 + ], + "InfestedCobblestone": [ + 164, + 168, + 184 + ], + "InfestedCrackedStoneBricks": [ + 164, + 168, + 184 + ], + "InfestedDeepslate": [ + 100, + 100, + 100 + ], + "InfestedMossyStoneBricks": [ + 164, + 168, + 184 + ], + "InfestedStone": [ + 164, + 168, + 184 + ], + "InfestedStoneBricks": [ + 164, + 168, + 184 + ], + "IronBlock": [ + 167, + 167, + 167 + ], + "IronDoor": [ + 167, + 167, + 167 + ], + "IronOre": [ + 112, + 112, + 112 + ], + "IronTrapdoor": [ + 167, + 167, + 167 + ], + "JackOLantern": [ + 216, + 127, + 51 + ], + "Jigsaw": [ + 153, + 153, + 153 + ], + "Jukebox": [ + 151, + 109, + 77 + ], + "JungleDoor": [ + 151, + 109, + 77 + ], + "JungleFence": [ + 151, + 109, + 77 + ], + "JungleFenceGate": [ + 151, + 109, + 77 + ], + "JunglePlanks": [ + 151, + 109, + 77 + ], + "JunglePressurePlate": [ + 151, + 109, + 77 + ], + "JungleSapling": [ + 0, + 124, + 0 + ], + "JungleSlab": [ + 151, + 109, + 77 + ], + "JungleTrapdoor": [ + 151, + 109, + 77 + ], + "JungleWood": [ + 151, + 109, + 77 + ], + "Kelp": [ + 64, + 64, + 255 + ], + "KelpPlant": [ + 64, + 64, + 255 + ], + "Lantern": [ + 167, + 167, + 167 + ], + "LapisBlock": [ + 74, + 128, + 255 + ], + "LapisOre": [ + 112, + 112, + 112 + ], + "LargeFern": [ + 0, + 124, + 0 + ], + "Lava": [ + 255, + 0, + 0 + ], + "LeafLitter": [ + 102, + 76, + 51 + ], + "Lectern": [ + 143, + 119, + 72 + ], + "Light": [ + 0, + 0, + 0 + ], + "LightBlueBanner": [ + 143, + 119, + 72 + ], + "LightBlueCarpet": [ + 102, + 153, + 216 + ], + "LightBlueConcrete": [ + 102, + 153, + 216 + ], + "LightBlueConcretePowder": [ + 102, + 153, + 216 + ], + "LightBlueGlazedTerracotta": [ + 102, + 153, + 216 + ], + "LightBlueTerracotta": [ + 112, + 108, + 138 + ], + "LightBlueWallBanner": [ + 143, + 119, + 72 + ], + "LightBlueWool": [ + 102, + 153, + 216 + ], + "LightGrayBanner": [ + 143, + 119, + 72 + ], + "LightGrayCarpet": [ + 153, + 153, + 153 + ], + "LightGrayConcrete": [ + 153, + 153, + 153 + ], + "LightGrayConcretePowder": [ + 153, + 153, + 153 + ], + "LightGrayGlazedTerracotta": [ + 153, + 153, + 153 + ], + "LightGrayTerracotta": [ + 135, + 107, + 98 + ], + "LightGrayWallBanner": [ + 143, + 119, + 72 + ], + "LightGrayWool": [ + 153, + 153, + 153 + ], + "LightWeightedPressurePlate": [ + 250, + 238, + 77 + ], + "LightningRod": [ + 216, + 127, + 51 + ], + "Lilac": [ + 0, + 124, + 0 + ], + "LilyOfTheValley": [ + 0, + 124, + 0 + ], + "LilyPad": [ + 0, + 124, + 0 + ], + "LimeBanner": [ + 143, + 119, + 72 + ], + "LimeCarpet": [ + 127, + 204, + 25 + ], + "LimeConcrete": [ + 127, + 204, + 25 + ], + "LimeConcretePowder": [ + 127, + 204, + 25 + ], + "LimeGlazedTerracotta": [ + 127, + 204, + 25 + ], + "LimeTerracotta": [ + 103, + 117, + 53 + ], + "LimeWallBanner": [ + 143, + 119, + 72 + ], + "LimeWool": [ + 127, + 204, + 25 + ], + "Lodestone": [ + 167, + 167, + 167 + ], + "Loom": [ + 143, + 119, + 72 + ], + "MagentaBanner": [ + 143, + 119, + 72 + ], + "MagentaCarpet": [ + 178, + 76, + 216 + ], + "MagentaConcrete": [ + 178, + 76, + 216 + ], + "MagentaConcretePowder": [ + 178, + 76, + 216 + ], + "MagentaGlazedTerracotta": [ + 178, + 76, + 216 + ], + "MagentaTerracotta": [ + 149, + 87, + 108 + ], + "MagentaWallBanner": [ + 143, + 119, + 72 + ], + "MagentaWool": [ + 178, + 76, + 216 + ], + "MagmaBlock": [ + 112, + 2, + 0 + ], + "MangroveDoor": [ + 153, + 51, + 51 + ], + "MangroveFence": [ + 153, + 51, + 51 + ], + "MangroveFenceGate": [ + 153, + 51, + 51 + ], + "MangrovePlanks": [ + 153, + 51, + 51 + ], + "MangrovePressurePlate": [ + 153, + 51, + 51 + ], + "MangrovePropagule": [ + 0, + 124, + 0 + ], + "MangroveRoots": [ + 129, + 86, + 49 + ], + "MangroveSlab": [ + 153, + 51, + 51 + ], + "MangroveTrapdoor": [ + 153, + 51, + 51 + ], + "MangroveWood": [ + 153, + 51, + 51 + ], + "Melon": [ + 127, + 204, + 25 + ], + "MelonStem": [ + 0, + 124, + 0 + ], + "MossBlock": [ + 102, + 127, + 51 + ], + "MossCarpet": [ + 102, + 127, + 51 + ], + "MossyCobblestone": [ + 112, + 112, + 112 + ], + "MossyStoneBricks": [ + 112, + 112, + 112 + ], + "MovingPiston": [ + 112, + 112, + 112 + ], + "Mud": [ + 87, + 92, + 92 + ], + "MudBrickSlab": [ + 135, + 107, + 98 + ], + "MudBricks": [ + 135, + 107, + 98 + ], + "MuddyMangroveRoots": [ + 129, + 86, + 49 + ], + "MushroomStem": [ + 199, + 199, + 199 + ], + "Mycelium": [ + 127, + 63, + 178 + ], + "NetherBrickFence": [ + 112, + 2, + 0 + ], + "NetherBrickSlab": [ + 112, + 2, + 0 + ], + "NetherBricks": [ + 112, + 2, + 0 + ], + "NetherGoldOre": [ + 112, + 2, + 0 + ], + "NetherQuartzOre": [ + 112, + 2, + 0 + ], + "NetherSprouts": [ + 76, + 127, + 153 + ], + "NetherWart": [ + 153, + 51, + 51 + ], + "NetherWartBlock": [ + 153, + 51, + 51 + ], + "NetheriteBlock": [ + 25, + 25, + 25 + ], + "Netherrack": [ + 112, + 2, + 0 + ], + "NoteBlock": [ + 143, + 119, + 72 + ], + "OakDoor": [ + 143, + 119, + 72 + ], + "OakFence": [ + 143, + 119, + 72 + ], + "OakFenceGate": [ + 143, + 119, + 72 + ], + "OakPlanks": [ + 143, + 119, + 72 + ], + "OakPressurePlate": [ + 143, + 119, + 72 + ], + "OakSapling": [ + 0, + 124, + 0 + ], + "OakShelf": [ + 143, + 119, + 72 + ], + "OakSign": [ + 143, + 119, + 72 + ], + "OakSlab": [ + 143, + 119, + 72 + ], + "OakTrapdoor": [ + 143, + 119, + 72 + ], + "OakWallSign": [ + 143, + 119, + 72 + ], + "OakWood": [ + 143, + 119, + 72 + ], + "Observer": [ + 112, + 112, + 112 + ], + "Obsidian": [ + 25, + 25, + 25 + ], + "OchreFroglight": [ + 247, + 233, + 163 + ], + "OpenEyeblossom": [ + 216, + 127, + 51 + ], + "OrangeBanner": [ + 143, + 119, + 72 + ], + "OrangeCarpet": [ + 216, + 127, + 51 + ], + "OrangeConcrete": [ + 216, + 127, + 51 + ], + "OrangeConcretePowder": [ + 216, + 127, + 51 + ], + "OrangeGlazedTerracotta": [ + 216, + 127, + 51 + ], + "OrangeTerracotta": [ + 159, + 82, + 36 + ], + "OrangeTulip": [ + 0, + 124, + 0 + ], + "OrangeWallBanner": [ + 143, + 119, + 72 + ], + "OrangeWool": [ + 216, + 127, + 51 + ], + "OxeyeDaisy": [ + 0, + 124, + 0 + ], + "OxidizedCopper": [ + 22, + 126, + 134 + ], + "OxidizedCopperBulb": [ + 22, + 126, + 134 + ], + "OxidizedCopperChest": [ + 22, + 126, + 134 + ], + "OxidizedCopperDoor": [ + 22, + 126, + 134 + ], + "OxidizedCopperGolemStatue": [ + 22, + 126, + 134 + ], + "OxidizedCopperGrate": [ + 22, + 126, + 134 + ], + "OxidizedCopperTrapdoor": [ + 22, + 126, + 134 + ], + "OxidizedLightningRod": [ + 22, + 126, + 134 + ], + "PackedIce": [ + 160, + 160, + 255 + ], + "PaleHangingMoss": [ + 153, + 153, + 153 + ], + "PaleMossBlock": [ + 153, + 153, + 153 + ], + "PaleMossCarpet": [ + 153, + 153, + 153 + ], + "PaleOakDoor": [ + 255, + 252, + 245 + ], + "PaleOakFence": [ + 255, + 252, + 245 + ], + "PaleOakFenceGate": [ + 255, + 252, + 245 + ], + "PaleOakHangingSign": [ + 255, + 252, + 245 + ], + "PaleOakLeaves": [ + 167, + 167, + 167 + ], + "PaleOakPlanks": [ + 255, + 252, + 245 + ], + "PaleOakPressurePlate": [ + 255, + 252, + 245 + ], + "PaleOakSapling": [ + 167, + 167, + 167 + ], + "PaleOakShelf": [ + 255, + 252, + 245 + ], + "PaleOakSign": [ + 255, + 252, + 245 + ], + "PaleOakSlab": [ + 255, + 252, + 245 + ], + "PaleOakTrapdoor": [ + 255, + 252, + 245 + ], + "PaleOakWallHangingSign": [ + 255, + 252, + 245 + ], + "PaleOakWallSign": [ + 255, + 252, + 245 + ], + "PaleOakWood": [ + 112, + 112, + 112 + ], + "PearlescentFroglight": [ + 242, + 127, + 165 + ], + "Peony": [ + 0, + 124, + 0 + ], + "PetrifiedOakSlab": [ + 143, + 119, + 72 + ], + "PinkBanner": [ + 143, + 119, + 72 + ], + "PinkCarpet": [ + 242, + 127, + 165 + ], + "PinkConcrete": [ + 242, + 127, + 165 + ], + "PinkConcretePowder": [ + 242, + 127, + 165 + ], + "PinkGlazedTerracotta": [ + 242, + 127, + 165 + ], + "PinkPetals": [ + 0, + 124, + 0 + ], + "PinkTerracotta": [ + 160, + 77, + 78 + ], + "PinkTulip": [ + 0, + 124, + 0 + ], + "PinkWallBanner": [ + 143, + 119, + 72 + ], + "PinkWool": [ + 242, + 127, + 165 + ], + "PistonHead": [ + 112, + 112, + 112 + ], + "PitcherCrop": [ + 0, + 124, + 0 + ], + "PitcherPlant": [ + 0, + 124, + 0 + ], + "Podzol": [ + 129, + 86, + 49 + ], + "PointedDripstone": [ + 76, + 50, + 35 + ], + "PolishedAndesite": [ + 112, + 112, + 112 + ], + "PolishedBasalt": [ + 25, + 25, + 25 + ], + "PolishedBlackstonePressurePlate": [ + 25, + 25, + 25 + ], + "PolishedDiorite": [ + 255, + 252, + 245 + ], + "PolishedGranite": [ + 151, + 109, + 77 + ], + "Poppy": [ + 0, + 124, + 0 + ], + "Potatoes": [ + 0, + 124, + 0 + ], + "PowderSnow": [ + 255, + 255, + 255 + ], + "Prismarine": [ + 76, + 127, + 153 + ], + "PrismarineBrickSlab": [ + 92, + 219, + 213 + ], + "PrismarineBricks": [ + 92, + 219, + 213 + ], + "PrismarineSlab": [ + 76, + 127, + 153 + ], + "Pumpkin": [ + 216, + 127, + 51 + ], + "PumpkinStem": [ + 0, + 124, + 0 + ], + "PurpleBanner": [ + 143, + 119, + 72 + ], + "PurpleCarpet": [ + 127, + 63, + 178 + ], + "PurpleConcrete": [ + 127, + 63, + 178 + ], + "PurpleConcretePowder": [ + 127, + 63, + 178 + ], + "PurpleGlazedTerracotta": [ + 127, + 63, + 178 + ], + "PurpleTerracotta": [ + 122, + 73, + 88 + ], + "PurpleWallBanner": [ + 143, + 119, + 72 + ], + "PurpleWool": [ + 127, + 63, + 178 + ], + "PurpurBlock": [ + 178, + 76, + 216 + ], + "PurpurPillar": [ + 178, + 76, + 216 + ], + "PurpurSlab": [ + 178, + 76, + 216 + ], + "QuartzBlock": [ + 255, + 252, + 245 + ], + "QuartzPillar": [ + 255, + 252, + 245 + ], + "QuartzSlab": [ + 255, + 252, + 245 + ], + "RawCopperBlock": [ + 216, + 127, + 51 + ], + "RawGoldBlock": [ + 250, + 238, + 77 + ], + "RawIronBlock": [ + 216, + 175, + 147 + ], + "RedBanner": [ + 143, + 119, + 72 + ], + "RedCarpet": [ + 153, + 51, + 51 + ], + "RedConcrete": [ + 153, + 51, + 51 + ], + "RedConcretePowder": [ + 153, + 51, + 51 + ], + "RedGlazedTerracotta": [ + 153, + 51, + 51 + ], + "RedMushroom": [ + 153, + 51, + 51 + ], + "RedMushroomBlock": [ + 153, + 51, + 51 + ], + "RedNetherBricks": [ + 112, + 2, + 0 + ], + "RedSand": [ + 216, + 127, + 51 + ], + "RedSandstone": [ + 216, + 127, + 51 + ], + "RedSandstoneSlab": [ + 216, + 127, + 51 + ], + "RedTerracotta": [ + 142, + 60, + 46 + ], + "RedTulip": [ + 0, + 124, + 0 + ], + "RedWallBanner": [ + 143, + 119, + 72 + ], + "RedWool": [ + 153, + 51, + 51 + ], + "RedstoneBlock": [ + 255, + 0, + 0 + ], + "RedstoneLamp": [ + 159, + 82, + 36 + ], + "RedstoneOre": [ + 112, + 112, + 112 + ], + "ReinforcedDeepslate": [ + 100, + 100, + 100 + ], + "RepeatingCommandBlock": [ + 127, + 63, + 178 + ], + "ResinBlock": [ + 159, + 82, + 36 + ], + "ResinBrickSlab": [ + 159, + 82, + 36 + ], + "ResinBrickWall": [ + 159, + 82, + 36 + ], + "ResinBricks": [ + 159, + 82, + 36 + ], + "ResinClump": [ + 159, + 82, + 36 + ], + "RespawnAnchor": [ + 25, + 25, + 25 + ], + "RootedDirt": [ + 151, + 109, + 77 + ], + "RoseBush": [ + 0, + 124, + 0 + ], + "Sand": [ + 247, + 233, + 163 + ], + "Sandstone": [ + 247, + 233, + 163 + ], + "SandstoneSlab": [ + 247, + 233, + 163 + ], + "Scaffolding": [ + 247, + 233, + 163 + ], + "Sculk": [ + 25, + 25, + 25 + ], + "SculkCatalyst": [ + 25, + 25, + 25 + ], + "SculkSensor": [ + 76, + 127, + 153 + ], + "SculkShrieker": [ + 25, + 25, + 25 + ], + "SculkVein": [ + 25, + 25, + 25 + ], + "SeaLantern": [ + 255, + 252, + 245 + ], + "SeaPickle": [ + 102, + 127, + 51 + ], + "Seagrass": [ + 64, + 64, + 255 + ], + "ShortDryGrass": [ + 229, + 229, + 51 + ], + "ShortGrass": [ + 0, + 124, + 0 + ], + "Shroomlight": [ + 153, + 51, + 51 + ], + "SlimeBlock": [ + 127, + 178, + 56 + ], + "SmallDripleaf": [ + 0, + 124, + 0 + ], + "SmithingTable": [ + 143, + 119, + 72 + ], + "Smoker": [ + 112, + 112, + 112 + ], + "SmoothQuartz": [ + 255, + 252, + 245 + ], + "SmoothRedSandstone": [ + 216, + 127, + 51 + ], + "SmoothSandstone": [ + 247, + 233, + 163 + ], + "SmoothStone": [ + 112, + 112, + 112 + ], + "SmoothStoneSlab": [ + 112, + 112, + 112 + ], + "SnifferEgg": [ + 153, + 51, + 51 + ], + "Snow": [ + 255, + 255, + 255 + ], + "SnowBlock": [ + 255, + 255, + 255 + ], + "SoulCampfire": [ + 129, + 86, + 49 + ], + "SoulFire": [ + 102, + 153, + 216 + ], + "SoulLantern": [ + 167, + 167, + 167 + ], + "SoulSand": [ + 102, + 76, + 51 + ], + "SoulSoil": [ + 102, + 76, + 51 + ], + "Spawner": [ + 112, + 112, + 112 + ], + "Sponge": [ + 229, + 229, + 51 + ], + "SporeBlossom": [ + 0, + 124, + 0 + ], + "SpruceDoor": [ + 129, + 86, + 49 + ], + "SpruceFence": [ + 129, + 86, + 49 + ], + "SpruceFenceGate": [ + 129, + 86, + 49 + ], + "SprucePlanks": [ + 129, + 86, + 49 + ], + "SprucePressurePlate": [ + 129, + 86, + 49 + ], + "SpruceSapling": [ + 0, + 124, + 0 + ], + "SpruceSlab": [ + 129, + 86, + 49 + ], + "SpruceTrapdoor": [ + 129, + 86, + 49 + ], + "SpruceWallHangingSign": [ + 143, + 119, + 72 + ], + "SpruceWood": [ + 129, + 86, + 49 + ], + "Stone": [ + 112, + 112, + 112 + ], + "StoneBrickSlab": [ + 112, + 112, + 112 + ], + "StoneBricks": [ + 112, + 112, + 112 + ], + "StonePressurePlate": [ + 112, + 112, + 112 + ], + "StoneSlab": [ + 112, + 112, + 112 + ], + "Stonecutter": [ + 112, + 112, + 112 + ], + "StrippedAcaciaWood": [ + 216, + 127, + 51 + ], + "StrippedBirchWood": [ + 247, + 233, + 163 + ], + "StrippedCherryWood": [ + 160, + 77, + 78 + ], + "StrippedCrimsonHyphae": [ + 92, + 25, + 29 + ], + "StrippedDarkOakWood": [ + 102, + 76, + 51 + ], + "StrippedJungleWood": [ + 151, + 109, + 77 + ], + "StrippedOakWood": [ + 143, + 119, + 72 + ], + "StrippedPaleOakWood": [ + 255, + 252, + 245 + ], + "StrippedSpruceWood": [ + 129, + 86, + 49 + ], + "StrippedWarpedHyphae": [ + 86, + 44, + 62 + ], + "StructureBlock": [ + 153, + 153, + 153 + ], + "SugarCane": [ + 0, + 124, + 0 + ], + "Sunflower": [ + 0, + 124, + 0 + ], + "SuspiciousGravel": [ + 112, + 112, + 112 + ], + "SuspiciousSand": [ + 247, + 233, + 163 + ], + "SweetBerryBush": [ + 0, + 124, + 0 + ], + "TallDryGrass": [ + 229, + 229, + 51 + ], + "TallGrass": [ + 0, + 124, + 0 + ], + "TallSeagrass": [ + 64, + 64, + 255 + ], + "Target": [ + 255, + 252, + 245 + ], + "Terracotta": [ + 216, + 127, + 51 + ], + "TestBlock": [ + 153, + 153, + 153 + ], + "TintedGlass": [ + 76, + 76, + 76 + ], + "Tnt": [ + 255, + 0, + 0 + ], + "Torchflower": [ + 0, + 124, + 0 + ], + "TorchflowerCrop": [ + 0, + 124, + 0 + ], + "TrappedChest": [ + 143, + 119, + 72 + ], + "TrialSpawner": [ + 112, + 112, + 112 + ], + "TubeCoral": [ + 51, + 76, + 178 + ], + "TubeCoralBlock": [ + 51, + 76, + 178 + ], + "TubeCoralFan": [ + 51, + 76, + 178 + ], + "TubeCoralWallFan": [ + 51, + 76, + 178 + ], + "Tuff": [ + 57, + 41, + 35 + ], + "TurtleEgg": [ + 247, + 233, + 163 + ], + "TwistingVines": [ + 76, + 127, + 153 + ], + "TwistingVinesPlant": [ + 76, + 127, + 153 + ], + "Vault": [ + 112, + 112, + 112 + ], + "VerdantFroglight": [ + 127, + 167, + 150 + ], + "Vine": [ + 0, + 124, + 0 + ], + "WarpedDoor": [ + 58, + 142, + 140 + ], + "WarpedFence": [ + 58, + 142, + 140 + ], + "WarpedFenceGate": [ + 58, + 142, + 140 + ], + "WarpedFungus": [ + 76, + 127, + 153 + ], + "WarpedHangingSign": [ + 58, + 142, + 140 + ], + "WarpedHyphae": [ + 86, + 44, + 62 + ], + "WarpedNylium": [ + 22, + 126, + 134 + ], + "WarpedPlanks": [ + 58, + 142, + 140 + ], + "WarpedPressurePlate": [ + 58, + 142, + 140 + ], + "WarpedRoots": [ + 76, + 127, + 153 + ], + "WarpedShelf": [ + 58, + 142, + 140 + ], + "WarpedSign": [ + 58, + 142, + 140 + ], + "WarpedSlab": [ + 58, + 142, + 140 + ], + "WarpedTrapdoor": [ + 58, + 142, + 140 + ], + "WarpedWallHangingSign": [ + 58, + 142, + 140 + ], + "WarpedWallSign": [ + 58, + 142, + 140 + ], + "WarpedWartBlock": [ + 20, + 180, + 133 + ], + "Water": [ + 64, + 64, + 255 + ], + "WeatheredCopper": [ + 58, + 142, + 140 + ], + "WeatheredCopperBulb": [ + 58, + 142, + 140 + ], + "WeatheredCopperChest": [ + 58, + 142, + 140 + ], + "WeatheredCopperDoor": [ + 58, + 142, + 140 + ], + "WeatheredCopperGolemStatue": [ + 58, + 142, + 140 + ], + "WeatheredCopperGrate": [ + 58, + 142, + 140 + ], + "WeatheredCopperTrapdoor": [ + 58, + 142, + 140 + ], + "WeatheredLightningRod": [ + 58, + 142, + 140 + ], + "WeepingVines": [ + 112, + 2, + 0 + ], + "WeepingVinesPlant": [ + 112, + 2, + 0 + ], + "WetSponge": [ + 229, + 229, + 51 + ], + "WhiteBanner": [ + 143, + 119, + 72 + ], + "WhiteCarpet": [ + 255, + 255, + 255 + ], + "WhiteConcrete": [ + 255, + 255, + 255 + ], + "WhiteConcretePowder": [ + 255, + 255, + 255 + ], + "WhiteGlazedTerracotta": [ + 255, + 255, + 255 + ], + "WhiteTerracotta": [ + 209, + 177, + 161 + ], + "WhiteTulip": [ + 0, + 124, + 0 + ], + "WhiteWallBanner": [ + 143, + 119, + 72 + ], + "WhiteWool": [ + 255, + 255, + 255 + ], + "Wildflowers": [ + 0, + 124, + 0 + ], + "WitherRose": [ + 0, + 124, + 0 + ], + "YellowBanner": [ + 143, + 119, + 72 + ], + "YellowCarpet": [ + 229, + 229, + 51 + ], + "YellowConcrete": [ + 229, + 229, + 51 + ], + "YellowConcretePowder": [ + 229, + 229, + 51 + ], + "YellowGlazedTerracotta": [ + 229, + 229, + 51 + ], + "YellowTerracotta": [ + 186, + 133, + 36 + ], + "YellowWallBanner": [ + 143, + 119, + 72 + ], + "YellowWool": [ + 229, + 229, + 51 + ] + }, + "transparent": [ + "Air", + "Barrier", + "BlackStainedGlass", + "BlackStainedGlassPane", + "BlueStainedGlass", + "BlueStainedGlassPane", + "BrownStainedGlass", + "BrownStainedGlassPane", + "CaveAir", + "CyanStainedGlass", + "CyanStainedGlassPane", + "Glass", + "GlassPane", + "GrayStainedGlass", + "GrayStainedGlassPane", + "GreenStainedGlass", + "GreenStainedGlassPane", + "Light", + "LightBlueStainedGlass", + "LightBlueStainedGlassPane", + "LightGrayStainedGlass", + "LightGrayStainedGlassPane", + "LimeStainedGlass", + "LimeStainedGlassPane", + "MagentaStainedGlass", + "MagentaStainedGlassPane", + "OrangeStainedGlass", + "OrangeStainedGlassPane", + "PinkStainedGlass", + "PinkStainedGlassPane", + "PurpleStainedGlass", + "PurpleStainedGlassPane", + "RedStainedGlass", + "RedStainedGlassPane", + "StructureVoid", + "TintedGlass", + "VoidAir", + "WhiteStainedGlass", + "WhiteStainedGlassPane", + "YellowStainedGlass", + "YellowStainedGlassPane" + ], + "water": [ + "Water" + ], + "ice": [ + "Ice", + "PackedIce", + "BlueIce", + "FrostedIce" + ] +} \ No newline at end of file diff --git a/MinecraftClient/Tui/MinimapColorMap.cs b/MinecraftClient/Tui/MinimapColorMap.cs new file mode 100644 index 00000000..b0b09596 --- /dev/null +++ b/MinecraftClient/Tui/MinimapColorMap.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using Avalonia.Media; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Tui +{ + /// + /// Maps block Materials to minimap colors using data extracted from Minecraft's + /// official MapColor table. Colors are loaded from the embedded MinimapBlockColors.json + /// resource generated by tools/gen_block_color_map.py. + /// + public static class MinimapColorMap + { + public static readonly Color WaterColor = Color.FromRgb(64, 64, 255); + public static readonly Color IceColor = Color.FromRgb(160, 160, 255); + public static readonly Color LavaColor = Color.FromRgb(255, 100, 0); + public static readonly Color DefaultColor = Color.FromRgb(60, 60, 60); + public static readonly Color VoidColor = Color.FromRgb(0, 0, 0); + public static readonly Color CaveBorderColor = Color.FromRgb(16, 16, 16); + public static readonly Color CaveSolidColor = Color.FromRgb(24, 20, 18); + + private static readonly FrozenDictionary ColorTable; + private static readonly FrozenSet FullyTransparentMats; + private static readonly FrozenSet WaterMats; + private static readonly FrozenSet IceMats; + + static MinimapColorMap() + { + var colors = new Dictionary(); + var transparent = new HashSet(); + var water = new HashSet(); + var ice = new HashSet(); + + try + { + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("MinimapBlockColors.json"); + if (stream is not null) + { + using var doc = JsonDocument.Parse(stream); + var root = doc.RootElement; + + if (root.TryGetProperty("colors", out var colorsEl)) + { + foreach (var prop in colorsEl.EnumerateObject()) + { + if (!Enum.TryParse(prop.Name, out var mat)) + continue; + var arr = prop.Value; + if (arr.GetArrayLength() < 3) continue; + byte r = (byte)arr[0].GetInt32(); + byte g = (byte)arr[1].GetInt32(); + byte b = (byte)arr[2].GetInt32(); + colors[mat] = Color.FromRgb(r, g, b); + } + } + + if (root.TryGetProperty("transparent", out var transEl)) + { + foreach (var item in transEl.EnumerateArray()) + { + if (Enum.TryParse(item.GetString(), out var mat)) + transparent.Add(mat); + } + } + + if (root.TryGetProperty("water", out var waterEl)) + { + foreach (var item in waterEl.EnumerateArray()) + { + if (Enum.TryParse(item.GetString(), out var mat)) + water.Add(mat); + } + } + + if (root.TryGetProperty("ice", out var iceEl)) + { + foreach (var item in iceEl.EnumerateArray()) + { + if (Enum.TryParse(item.GetString(), out var mat)) + ice.Add(mat); + } + } + } + } + catch (Exception ex) + { + ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Failed to load color data: {ex.Message}"); + } + + if (transparent.Count == 0) + { + transparent.Add(Material.Air); + transparent.Add(Material.CaveAir); + transparent.Add(Material.VoidAir); + } + if (water.Count == 0) + water.Add(Material.Water); + if (ice.Count == 0) + { + ice.Add(Material.Ice); + ice.Add(Material.PackedIce); + ice.Add(Material.BlueIce); + ice.Add(Material.FrostedIce); + } + + ColorTable = colors.ToFrozenDictionary(); + FullyTransparentMats = transparent.ToFrozenSet(); + WaterMats = water.ToFrozenSet(); + IceMats = ice.ToFrozenSet(); + } + + public static bool IsFullyTransparent(Material m) => FullyTransparentMats.Contains(m); + + /// + /// Returns true for materials that block light propagation (solid, liquids), + /// used by cave mode to find the surface from the player's Y level. + /// Mirrors VoxelMap's lightDampening > 0 check. + /// + public static bool IsLightBlocking(Material m) + => (m == Material.Lava) || (!FullyTransparentMats.Contains(m) && m.IsSolid()); + + public static bool IsWater(Material m) => WaterMats.Contains(m); + + public static bool IsIce(Material m) => IceMats.Contains(m); + + public static Color GetBaseColor(Material m) + { + if (m == Material.Lava) + return LavaColor; + return ColorTable.GetValueOrDefault(m, DefaultColor); + } + + /// + /// Apply Minecraft-style height shading. The shade multiplier depends on + /// the height difference between the current block and the block to its north. + /// Vanilla maps use four brightness levels: LOW (180/255), NORMAL (220/255), + /// HIGH (255/255), and LOWEST (135/255). We use NORMAL as baseline and shift + /// up/down based on delta. + /// + public static Color ApplyHeightShade(Color baseColor, int heightDelta) + { + int multiplier = heightDelta switch + { + > 0 => 255, // higher than neighbor: brightest + 0 => 220, // same height: normal + _ => 180, // lower than neighbor: darker + }; + byte r = (byte)(baseColor.R * multiplier / 255); + byte g = (byte)(baseColor.G * multiplier / 255); + byte b = (byte)(baseColor.B * multiplier / 255); + return Color.FromRgb(r, g, b); + } + + public static Color BlendWaterColor(Color bottomColor, int waterDepth) + { + double alpha = Math.Min(0.85, 0.35 + waterDepth * 0.08); + return Blend(WaterColor, bottomColor, alpha); + } + + public static Color BlendIceColor(Color bottomColor) + { + return Blend(IceColor, bottomColor, 0.35); + } + + /// + /// Darken a color to simulate underground lighting. Cave floors receive + /// a minimum brightness of ~32/255 for non-solid blocks (matching VoxelMap), + /// while solid/unreachable columns render as near-black. + /// + public static Color ApplyCaveDarkening(Color baseColor, double factor = 0.55) + { + byte r = (byte)(baseColor.R * factor); + byte g = (byte)(baseColor.G * factor); + byte b = (byte)(baseColor.B * factor); + return Color.FromRgb(r, g, b); + } + + private static Color Blend(Color top, Color bottom, double topAlpha) + { + byte r = (byte)(top.R * topAlpha + bottom.R * (1.0 - topAlpha)); + byte g = (byte)(top.G * topAlpha + bottom.G * (1.0 - topAlpha)); + byte b = (byte)(top.B * topAlpha + bottom.B * (1.0 - topAlpha)); + return Color.FromRgb(r, g, b); + } + } +} diff --git a/MinecraftClient/Tui/MinimapControl.cs b/MinecraftClient/Tui/MinimapControl.cs new file mode 100644 index 00000000..616b44e5 --- /dev/null +++ b/MinecraftClient/Tui/MinimapControl.cs @@ -0,0 +1,1267 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Threading; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Tui +{ + public enum CaveModeOption { auto, on, off } + + /// + /// TUI minimap control rendered as a grid of TextBlocks using half-block characters. + /// Zoom is expressed as blocks-per-pixel (1 = 1:1, 16 = 16 blocks per pixel). + /// Entity names are drawn directly on the map below their icon. + /// + public class MinimapControl : UserControl + { + public const int MinZoom = 1; + public const int MaxZoom = 16; + public const int DefaultZoom = 2; + public const int DefaultWidth = 40; + public const int DefaultHeight = 40; + public const int DefaultRefreshMs = 1000; + public const int MinRefreshMs = 100; + public const int MaxRefreshMs = 5000; + + private int _mapWidth; + private int _mapHeight; + private int _cellRows; + + private int _blocksPerPixel = DefaultZoom; + private volatile bool _sampling; + private CancellationTokenSource? _cts; + + private readonly NameDisplayConfig _nameConfig = new(); + + private TextBlock[,] _cells; + private readonly StackPanel _infoRow; + private readonly StackPanel _legendPanel; + private readonly Grid _mapGrid; + private readonly DispatcherTimer _timer; + + private SampleResult? _lastResult; + private int _hoverCol = -1; + private int _hoverRow = -1; + private double _hoverGlobalX; + private double _hoverGlobalY; + + public int BlocksPerPixel + { + get => _blocksPerPixel; + set => _blocksPerPixel = Math.Clamp(value, MinZoom, MaxZoom); + } + + public NameDisplayConfig NameConfig => _nameConfig; + + public TuiTooltipService? TooltipService { get; set; } + + public MinimapPosition Position { get; set; } = MinimapPosition.top_right; + + public CaveModeOption CaveMode { get; set; } = CaveModeOption.auto; + + public int MapPixelWidth => _mapWidth; + public int MapPixelHeight => _mapHeight; + + public int RefreshIntervalMs + { + get => (int)_timer.Interval.TotalMilliseconds; + set => _timer.Interval = TimeSpan.FromMilliseconds(Math.Clamp(value, MinRefreshMs, MaxRefreshMs)); + } + + public MinimapControl() : this(DefaultWidth, DefaultHeight) { } + + public MinimapControl(int width, int height) + { + _mapWidth = Math.Max(10, width); + _mapHeight = Math.Max(4, height % 2 == 0 ? height : height + 1); + _cellRows = _mapHeight / 2; + + _mapGrid = new Grid(); + _cells = BuildGrid(_mapGrid, _cellRows, _mapWidth); + + _infoRow = new StackPanel { Orientation = Orientation.Horizontal }; + _legendPanel = new StackPanel { Orientation = Orientation.Horizontal }; + + var root = new StackPanel + { + Orientation = Orientation.Vertical, + Children = { _mapGrid, _infoRow, _legendPanel }, + }; + + Content = root; + + _mapGrid.PointerMoved += OnMapPointerMoved; + _mapGrid.PointerExited += OnMapPointerExited; + + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(DefaultRefreshMs), + }; + _timer.Tick += (_, _) => RequestSample(); + } + + public void Resize(int width, int height) + { + _mapWidth = Math.Max(10, width); + _mapHeight = Math.Max(4, height % 2 == 0 ? height : height + 1); + _cellRows = _mapHeight / 2; + + _mapGrid.Children.Clear(); + _mapGrid.RowDefinitions.Clear(); + _mapGrid.ColumnDefinitions.Clear(); + _cells = BuildGrid(_mapGrid, _cellRows, _mapWidth); + } + + private static TextBlock[,] BuildGrid(Grid grid, int rows, int cols) + { + var cells = new TextBlock[rows, cols]; + for (int r = 0; r < rows; r++) + grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + for (int c = 0; c < cols; c++) + grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + var tb = new TextBlock + { + Text = "\u2580", + Foreground = Brushes.Black, + Background = Brushes.Black, + Padding = new Thickness(0), + Margin = new Thickness(0), + FontSize = 1, + }; + Grid.SetRow(tb, r); + Grid.SetColumn(tb, c); + grid.Children.Add(tb); + cells[r, c] = tb; + } + } + return cells; + } + + public void Start() + { + _cts = new CancellationTokenSource(); + _timer.Start(); + RequestSample(); + } + + public void Stop() + { + _timer.Stop(); + _cts?.Cancel(); + _cts?.Dispose(); + _cts = null; + } + + private void RequestSample() + { + if (_sampling) return; + if (McClient.Instance is not McClient client) return; + if (!client.GetTerrainEnabled()) return; + + _sampling = true; + var ct = _cts?.Token ?? CancellationToken.None; + int bpp = _blocksPerPixel; + int w = _mapWidth; + int h = _mapHeight; + + bool showPlayers = _nameConfig.Players; + bool showHostile = _nameConfig.Hostile; + bool showNeutral = _nameConfig.Neutral; + bool showPassive = _nameConfig.Passive; + var caveOpt = CaveMode; + + Task.Run(() => + { + try + { + var result = SampleTerrain(client, bpp, w, h, + showPlayers, showHostile, showNeutral, showPassive, caveOpt, ct); + if (ct.IsCancellationRequested) return; + + Dispatcher.UIThread.Post(() => + { + ApplyPixelBuffer(result, w, h); + UpdateInfoBarAndLegend(client, bpp, result.VisibleCategories, w, + result.CaveModeActive); + }); + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Sample error: {ex.Message}"); + } + finally + { + _sampling = false; + } + }, ct); + } + + internal sealed class EntityLabel + { + public string Name = ""; + public Color LabelColor; + public int PixelX; + public int PixelY; + } + + internal sealed class PixelEntityInfo + { + public string Name = ""; + public MobCategory Category; + public double X, Y, Z; + public float Health; + public float MaxHealth; + public int Priority; + } + + private sealed class SampleResult + { + public Color[,] Pixels = null!; + public (char Ch, Color Fg, Color Bg)?[,] CharOverlay = null!; + public HashSet VisibleCategories = []; + public int[,] Heights = null!; + public Material[,]? BlockTypes; + public List<(Material Mat, int Count)>?[,]? BlockSummary; + public List?[,]? EntityMap; + public int PlayerBlockX; + public int PlayerBlockZ; + public int CenterX; + public int CenterY; + public int Bpp; + public bool CaveModeActive; + } + + private static bool ShouldShowNameLocal(MobCategory cat, + bool showPlayers, bool showHostile, bool showNeutral, bool showPassive) + { + return cat switch + { + MobCategory.Player => showPlayers, + MobCategory.Hostile => showHostile, + MobCategory.Neutral => showNeutral, + MobCategory.Passive => showPassive, + _ => false, + }; + } + + private static SampleResult SampleTerrain(McClient client, int bpp, int mapW, int mapH, + bool showPlayers, bool showHostile, bool showNeutral, bool showPassive, + CaveModeOption caveOpt, CancellationToken ct) + { + var result = new SampleResult + { + Pixels = new Color[mapW, mapH], + CharOverlay = new (char, Color, Color)?[mapW, mapH / 2], + Heights = new int[mapW, mapH], + EntityMap = new List?[mapW, mapH], + BlockTypes = bpp == 1 ? new Material[mapW, mapH] : null, + BlockSummary = bpp > 1 ? new List<(Material, int)>?[mapW, mapH] : null, + Bpp = bpp, + }; + var world = client.GetWorld(); + var playerLoc = client.GetCurrentLocation(); + + int playerBlockX = (int)Math.Floor(playerLoc.X); + int playerBlockZ = (int)Math.Floor(playerLoc.Z); + int playerBlockY = (int)Math.Floor(playerLoc.Y); + + result.PlayerBlockX = playerBlockX; + result.PlayerBlockZ = playerBlockZ; + result.CenterX = mapW / 2; + result.CenterY = mapH / 2; + + var dim = World.GetDimension(); + int minY = dim.minY; + int scanTop = Math.Min(playerBlockY + 32, dim.maxY - 1); + + bool caveMode = ResolveCaveMode(caveOpt, world, dim, playerBlockX, playerBlockY, playerBlockZ, scanTop); + result.CaveModeActive = caveMode; + + var entities = client.GetEntityHandlingEnabled() + ? client.GetEntities() + : null; + + var entityPixels = new Dictionary<(int, int), (Color Color, int Priority)>(); + int centerX = mapW / 2; + int centerY = mapH / 2; + + var nameLabels = new List(); + var uuidNameMap = client.GetOnlinePlayersWithUUID(); + + if (entities is not null) + { + int playerEntityId = client.GetPlayerEntityID(); + foreach (var kvp in entities) + { + if (ct.IsCancellationRequested) return result; + var entity = kvp.Value; + var cat = MinimapEntityClassifier.Classify(entity.Type); + if (cat == MobCategory.NonLiving) continue; + if (kvp.Key == playerEntityId) continue; + + if (!MinimapEntityClassifier.ShouldDisplay(cat, playerLoc.Y, entity.Location.Y)) + continue; + + double relX = (entity.Location.X - playerLoc.X) / bpp; + double relZ = (entity.Location.Z - playerLoc.Z) / bpp; + int px = (int)Math.Floor(relX) + centerX; + int py = (int)Math.Floor(relZ) + centerY; + + if (px < 0 || px >= mapW || py < 0 || py >= mapH) continue; + + var baseColor = MinimapEntityClassifier.GetBaseColor(cat); + Color color; + if (cat == MobCategory.Player) + color = baseColor; + else + color = MinimapEntityClassifier.ApplyDepthFade(baseColor, playerLoc.Y, entity.Location.Y); + int priority = MinimapEntityClassifier.GetPriority(cat); + + var key = (px, py); + if (!entityPixels.TryGetValue(key, out var existing) || priority > existing.Priority) + entityPixels[key] = (color, priority); + + result.VisibleCategories.Add(cat); + + string eName = ResolveEntityName(client, entity, cat, uuidNameMap); + var pixelList = result.EntityMap![px, py] ??= []; + pixelList.Add(new PixelEntityInfo + { + Name = eName, + Category = cat, + X = entity.Location.X, + Y = entity.Location.Y, + Z = entity.Location.Z, + Health = entity.Health, + MaxHealth = -1, + Priority = priority, + }); + + if (ShouldShowNameLocal(cat, showPlayers, showHostile, showNeutral, showPassive)) + { + string name = ResolveEntityName(client, entity, cat, uuidNameMap); + nameLabels.Add(new EntityLabel + { + Name = name, + LabelColor = color, + PixelX = px, + PixelY = py, + }); + } + } + } + + entityPixels[(centerX, centerY)] = (MinimapEntityClassifier.PlayerColor, 5); + result.VisibleCategories.Add(MobCategory.Player); + + var selfList = result.EntityMap![centerX, centerY] ??= []; + selfList.Add(new PixelEntityInfo + { + Name = client.GetUsername(), + Category = MobCategory.Player, + X = playerLoc.X, + Y = playerLoc.Y, + Z = playerLoc.Z, + Health = client.GetHealth(), + MaxHealth = 20f, + Priority = 5, + }); + + ChunkColumn? cachedColumn = null; + int cachedChunkX = int.MinValue, cachedChunkZ = int.MinValue; + + bool[,]? caveMask = caveMode ? new bool[mapW, mapH] : null; + + for (int px = 0; px < mapW; px++) + { + for (int py = 0; py < mapH; py++) + { + if (ct.IsCancellationRequested) return result; + + int baseX = playerBlockX + (px - centerX) * bpp; + int baseZ = playerBlockZ + (py - centerY) * bpp; + + if (caveMode) + { + if (bpp == 1) + { + var (color, surfY, surfMat, inCave) = SampleColumnCave( + world, baseX, baseZ, playerBlockY, minY, dim.maxY - 1, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + result.BlockTypes![px, py] = surfMat; + caveMask![px, py] = inCave; + } + else + { + var (color, surfY, matSum, inCave) = SampleAreaDominantCave( + world, baseX, baseZ, bpp, playerBlockY, minY, dim.maxY - 1, + result.BlockSummary is not null, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + if (result.BlockSummary is not null) + result.BlockSummary[px, py] = matSum; + caveMask![px, py] = inCave; + } + } + else + { + if (bpp == 1) + { + var (color, surfY, surfMat) = SampleColumn(world, baseX, baseZ, scanTop, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + result.BlockTypes![px, py] = surfMat; + } + else + { + var (color, surfY, matSum) = SampleAreaDominant(world, baseX, baseZ, bpp, + scanTop, minY, result.BlockSummary is not null, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + result.Pixels[px, py] = color; + result.Heights[px, py] = surfY; + if (result.BlockSummary is not null) + result.BlockSummary[px, py] = matSum; + } + } + } + } + + for (int px = 0; px < mapW; px++) + { + for (int py = 0; py < mapH; py++) + { + if (entityPixels.ContainsKey((px, py))) continue; + + int northHeight = py > 0 ? result.Heights[px, py - 1] : result.Heights[px, py]; + int delta = result.Heights[px, py] - northHeight; + result.Pixels[px, py] = MinimapColorMap.ApplyHeightShade(result.Pixels[px, py], delta); + } + } + + if (caveMask is not null) + ApplyCaveBorder(result, caveMask, mapW, mapH, entityPixels); + + foreach (var (key, info) in entityPixels) + { + var (px, py) = key; + if (px >= 0 && px < mapW && py >= 0 && py < mapH) + result.Pixels[px, py] = info.Color; + } + + BakeNameLabels(result, nameLabels, mapW, mapH); + + return result; + } + + private static string ResolveEntityName(McClient client, Entity entity, + MobCategory cat, Dictionary? uuidNameMap) + { + if (cat == MobCategory.Player) + { + if (!string.IsNullOrWhiteSpace(entity.Name)) + return entity.Name; + + if (entity.UUID != System.Guid.Empty) + { + var playerInfo = client.GetPlayerInfo(entity.UUID); + if (!string.IsNullOrWhiteSpace(playerInfo?.Name)) + return playerInfo.Name; + + if (uuidNameMap is not null && + uuidNameMap.TryGetValue(entity.UUID.ToString(), out string? mapped) && + !string.IsNullOrWhiteSpace(mapped)) + return mapped; + } + + return "Player"; + } + + if (!string.IsNullOrWhiteSpace(entity.Name)) + return entity.Name; + + return entity.Type.ToString(); + } + + private static void BakeNameLabels(SampleResult result, List labels, + int mapW, int mapH) + { + if (labels.Count == 0) return; + int cellRows = mapH / 2; + + var occupied = new HashSet<(int col, int row)>(); + + labels.Sort((a, b) => + { + int pa = MinimapEntityClassifier.GetPriority( + a.LabelColor == MinimapEntityClassifier.PlayerColor ? MobCategory.Player : + a.LabelColor == MinimapEntityClassifier.HostileColor ? MobCategory.Hostile : + a.LabelColor == MinimapEntityClassifier.NeutralColor ? MobCategory.Neutral : MobCategory.Passive); + int pb = MinimapEntityClassifier.GetPriority( + b.LabelColor == MinimapEntityClassifier.PlayerColor ? MobCategory.Player : + b.LabelColor == MinimapEntityClassifier.HostileColor ? MobCategory.Hostile : + b.LabelColor == MinimapEntityClassifier.NeutralColor ? MobCategory.Neutral : MobCategory.Passive); + return pb.CompareTo(pa); + }); + + foreach (var lbl in labels) + { + int cellRow = (lbl.PixelY / 2) + 1; + if (cellRow >= cellRows) cellRow = lbl.PixelY / 2 - 1; + if (cellRow < 0 || cellRow >= cellRows) continue; + + int startCol = lbl.PixelX - lbl.Name.Length / 2; + startCol = Math.Clamp(startCol, 0, mapW - 1); + + bool fits = true; + int endCol = Math.Min(startCol + lbl.Name.Length, mapW); + for (int c = startCol; c < endCol; c++) + { + if (occupied.Contains((c, cellRow))) + { + fits = false; + break; + } + } + if (!fits) continue; + + for (int i = 0; i < lbl.Name.Length && startCol + i < mapW; i++) + { + int col = startCol + i; + occupied.Add((col, cellRow)); + + var bgTop = result.Pixels[col, cellRow * 2]; + var bgBot = (cellRow * 2 + 1 < mapH) + ? result.Pixels[col, cellRow * 2 + 1] + : bgTop; + + var avgBg = Color.FromRgb( + (byte)((bgTop.R + bgBot.R) / 2), + (byte)((bgTop.G + bgBot.G) / 2), + (byte)((bgTop.B + bgBot.B) / 2)); + + result.CharOverlay[col, cellRow] = (lbl.Name[i], lbl.LabelColor, avgBg); + } + } + } + + private static (Color color, int surfaceY, Material surfaceMat) SampleColumn(World world, int x, int z, + int scanTop, int minY, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + int chunkX = x >> 4; + int chunkZ = z >> 4; + if (chunkX != cachedChunkX || chunkZ != cachedChunkZ) + { + cachedColumn = world[chunkX, chunkZ]; + cachedChunkX = chunkX; + cachedChunkZ = chunkZ; + } + + if (cachedColumn is null) + return (MinimapColorMap.VoidColor, minY, Material.Air); + + int waterDepth = 0; + bool inIce = false; + int surfaceY = minY; + Material topMat = Material.Air; + + for (int y = scanTop; y >= minY; y--) + { + var loc = new Mapping.Location(x, y, z); + var chunk = cachedColumn.GetChunk(loc); + if (chunk is null) continue; + + var block = chunk.GetBlock(loc); + var mat = block.Type; + + if (MinimapColorMap.IsFullyTransparent(mat)) + continue; + + if (MinimapColorMap.IsWater(mat)) + { + if (waterDepth == 0) { surfaceY = y; topMat = mat; } + waterDepth++; + continue; + } + + if (MinimapColorMap.IsIce(mat) && !inIce) + { + if (waterDepth == 0) { surfaceY = y; topMat = mat; } + inIce = true; + continue; + } + + if (waterDepth == 0 && !inIce) { surfaceY = y; topMat = mat; } + + var baseColor = MinimapColorMap.GetBaseColor(mat); + + if (waterDepth > 0) + baseColor = MinimapColorMap.BlendWaterColor(baseColor, waterDepth); + if (inIce) + baseColor = MinimapColorMap.BlendIceColor(baseColor); + + return (baseColor, surfaceY, topMat); + } + + if (waterDepth > 0) + return (MinimapColorMap.WaterColor, surfaceY, topMat); + + return (MinimapColorMap.VoidColor, minY, Material.Air); + } + + private static (Color color, int surfaceY, List<(Material Mat, int Count)>? matSummary) + SampleAreaDominant(World world, int baseX, int baseZ, + int size, int scanTop, int minY, bool collectMats, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + var colorCounts = new Dictionary(); + Dictionary? matCounts = collectMats ? [] : null; + + int step = Math.Max(1, size / 3); + for (int dx = 0; dx < size; dx += step) + { + for (int dz = 0; dz < size; dz += step) + { + var (c, surfY, surfMat) = SampleColumn(world, baseX + dx, baseZ + dz, scanTop, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + + if (colorCounts.TryGetValue(c, out var existing)) + colorCounts[c] = (existing.Count + 1, existing.SumY + surfY); + else + colorCounts[c] = (1, surfY); + + if (matCounts is not null) + { + if (matCounts.TryGetValue(surfMat, out int mc)) + matCounts[surfMat] = mc + 1; + else + matCounts[surfMat] = 1; + } + } + } + + Color best = MinimapColorMap.VoidColor; + int bestCount = 0; + int avgY = minY; + foreach (var kvp in colorCounts) + { + if (kvp.Value.Count > bestCount) + { + bestCount = kvp.Value.Count; + best = kvp.Key; + avgY = kvp.Value.SumY / kvp.Value.Count; + } + } + + List<(Material, int)>? summary = null; + if (matCounts is not null && matCounts.Count > 0) + { + summary = matCounts + .OrderByDescending(kv => kv.Value) + .Select(kv => (kv.Key, kv.Value)) + .ToList(); + } + + return (best, avgY, summary); + } + + /// + /// Determine whether cave mode should be active for this frame. + /// Mirrors VoxelMap's detection: hasCeiling dimensions always use cave mode, + /// otherwise check whether the player's column has a solid block above. + /// + private static bool ResolveCaveMode(CaveModeOption opt, World world, Dimension dim, + int playerX, int playerY, int playerZ, int scanTop) + { + if (opt == CaveModeOption.off) return false; + if (opt == CaveModeOption.on) return true; + + if (dim.hasCeiling) return true; + + for (int y = playerY + 2; y <= scanTop; y++) + { + var mat = world.GetBlock(new Mapping.Location(playerX, y, playerZ)).Type; + if (MinimapColorMap.IsLightBlocking(mat)) + return true; + } + return false; + } + + /// + /// Cave-mode column sampler. Starting from playerY, scans down through air + /// to find the first light-blocking block (the cave floor), or scans up if + /// the player is embedded in solid. Returns the floor block color with cave + /// darkening applied, plus an inCave flag indicating the column has a reachable + /// air pocket at the player's Y level. + /// + private static (Color color, int surfaceY, Material surfaceMat, bool inCave) SampleColumnCave( + World world, int x, int z, int playerY, int minY, int maxY, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + int chunkX = x >> 4; + int chunkZ = z >> 4; + if (chunkX != cachedChunkX || chunkZ != cachedChunkZ) + { + cachedColumn = world[chunkX, chunkZ]; + cachedChunkX = chunkX; + cachedChunkZ = chunkZ; + } + + if (cachedColumn is null) + return (MinimapColorMap.VoidColor, minY, Material.Air, false); + + int caveFloorY = FindCaveFloorY(cachedColumn, x, z, playerY, minY, maxY); + + if (caveFloorY == int.MinValue) + { + var fallback = SampleColumn(world, x, z, maxY, minY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + return (MinimapColorMap.CaveSolidColor, fallback.surfaceY, fallback.surfaceMat, false); + } + + var loc = new Mapping.Location(x, caveFloorY, z); + var chunk = cachedColumn.GetChunk(loc); + if (chunk is null) + return (MinimapColorMap.CaveSolidColor, caveFloorY, Material.Air, false); + + var block = chunk.GetBlock(loc); + var mat = block.Type; + var color = MinimapColorMap.GetBaseColor(mat); + color = MinimapColorMap.ApplyCaveDarkening(color); + + return (color, caveFloorY, mat, true); + } + + /// + /// Find the cave floor Y at (x, z) by scanning from playerY. + /// If the block at playerY is air-like, scan down for the first solid block. + /// If the block at playerY is solid, scan up (up to playerY + 10) for the + /// first air block, then return that Y (the cave ceiling opening). + /// Returns int.MinValue if no cave floor is found. + /// + private static int FindCaveFloorY(ChunkColumn column, int x, int z, int playerY, int minY, int maxY) + { + var startLoc = new Mapping.Location(x, playerY, z); + var startChunk = column.GetChunk(startLoc); + + bool startIsAir; + if (startChunk is null) + { + startIsAir = true; + } + else + { + var startMat = startChunk.GetBlock(startLoc).Type; + startIsAir = !MinimapColorMap.IsLightBlocking(startMat); + } + + if (startIsAir) + { + for (int y = playerY - 1; y >= minY; y--) + { + var loc = new Mapping.Location(x, y, z); + var chunk = column.GetChunk(loc); + if (chunk is null) continue; + + var mat = chunk.GetBlock(loc).Type; + if (MinimapColorMap.IsLightBlocking(mat)) + return y; + } + return minY; + } + else + { + int upLimit = Math.Min(playerY + 10, maxY); + for (int y = playerY + 1; y <= upLimit; y++) + { + var loc = new Mapping.Location(x, y, z); + var chunk = column.GetChunk(loc); + if (chunk is null) continue; + + var mat = chunk.GetBlock(loc).Type; + if (!MinimapColorMap.IsLightBlocking(mat)) + { + for (int y2 = y - 1; y2 >= minY; y2--) + { + var loc2 = new Mapping.Location(x, y2, z); + var chunk2 = column.GetChunk(loc2); + if (chunk2 is null) continue; + + var mat2 = chunk2.GetBlock(loc2).Type; + if (MinimapColorMap.IsLightBlocking(mat2)) + return y2; + } + return minY; + } + } + return int.MinValue; + } + } + + private static (Color color, int surfaceY, List<(Material Mat, int Count)>? matSummary, bool inCave) + SampleAreaDominantCave(World world, int baseX, int baseZ, + int size, int playerY, int minY, int maxY, bool collectMats, + ref ChunkColumn? cachedColumn, ref int cachedChunkX, ref int cachedChunkZ) + { + var colorCounts = new Dictionary(); + Dictionary? matCounts = collectMats ? [] : null; + int caveCount = 0; + + int step = Math.Max(1, size / 3); + for (int dx = 0; dx < size; dx += step) + { + for (int dz = 0; dz < size; dz += step) + { + var (c, surfY, surfMat, inCave) = SampleColumnCave( + world, baseX + dx, baseZ + dz, playerY, minY, maxY, + ref cachedColumn, ref cachedChunkX, ref cachedChunkZ); + + if (inCave) caveCount++; + + if (colorCounts.TryGetValue(c, out var existing)) + colorCounts[c] = (existing.Count + 1, existing.SumY + surfY); + else + colorCounts[c] = (1, surfY); + + if (matCounts is not null) + { + if (matCounts.TryGetValue(surfMat, out int mc)) + matCounts[surfMat] = mc + 1; + else + matCounts[surfMat] = 1; + } + } + } + + Color best = MinimapColorMap.VoidColor; + int bestCount = 0; + int avgY = minY; + foreach (var kvp in colorCounts) + { + if (kvp.Value.Count > bestCount) + { + bestCount = kvp.Value.Count; + best = kvp.Key; + avgY = kvp.Value.SumY / kvp.Value.Count; + } + } + + List<(Material, int)>? summary = null; + if (matCounts is not null && matCounts.Count > 0) + { + summary = matCounts + .OrderByDescending(kv => kv.Value) + .Select(kv => (kv.Key, kv.Value)) + .ToList(); + } + + int totalSamples = 0; + foreach (var kvp in colorCounts) + totalSamples += kvp.Value.Count; + + bool majorityInCave = caveCount * 2 >= totalSamples; + return (best, avgY, summary, majorityInCave); + } + + /// + /// Draw a 1-pixel dark border around the boundary between cave-reachable pixels + /// and non-cave (solid/surface) pixels, giving the cave region a visible edge. + /// + private static void ApplyCaveBorder(SampleResult result, bool[,] caveMask, + int mapW, int mapH, Dictionary<(int, int), (Color, int)> entityPixels) + { + for (int px = 0; px < mapW; px++) + { + for (int py = 0; py < mapH; py++) + { + if (entityPixels.ContainsKey((px, py))) continue; + if (caveMask[px, py]) continue; + + bool neighborInCave = false; + if (px > 0 && caveMask[px - 1, py]) neighborInCave = true; + if (!neighborInCave && px < mapW - 1 && caveMask[px + 1, py]) neighborInCave = true; + if (!neighborInCave && py > 0 && caveMask[px, py - 1]) neighborInCave = true; + if (!neighborInCave && py < mapH - 1 && caveMask[px, py + 1]) neighborInCave = true; + + if (neighborInCave) + result.Pixels[px, py] = MinimapColorMap.CaveBorderColor; + } + } + } + + private void ApplyPixelBuffer(SampleResult result, int w, int h) + { + int rows = h / 2; + for (int row = 0; row < rows && row < _cellRows; row++) + { + for (int col = 0; col < w && col < _mapWidth; col++) + { + var overlay = result.CharOverlay[col, row]; + if (overlay is not null) + { + var (ch, fg, bg) = overlay.Value; + _cells[row, col].Text = ch.ToString(); + _cells[row, col].Foreground = new SolidColorBrush(fg); + _cells[row, col].Background = new SolidColorBrush(bg); + } + else + { + var topColor = result.Pixels[col, row * 2]; + var bottomColor = result.Pixels[col, row * 2 + 1]; + + _cells[row, col].Text = "\u2580"; + _cells[row, col].Foreground = new SolidColorBrush(topColor); + _cells[row, col].Background = new SolidColorBrush(bottomColor); + } + } + } + + _lastResult = result; + + if (_hoverCol >= 0 && _hoverRow >= 0) + UpdateTooltip(_hoverCol, _hoverRow); + } + + private void OnMapPointerMoved(object? sender, PointerEventArgs e) + { + var pos = e.GetPosition(_mapGrid); + int col = (int)pos.X; + int row = (int)pos.Y; + + if (col < 0 || col >= _mapWidth || row < 0 || row >= _cellRows) + { + HideTooltip(); + return; + } + + _hoverCol = col; + _hoverRow = row; + + if (this.VisualRoot is Visual root + && _mapGrid.TranslatePoint(pos, root) is { } gp) + { + _hoverGlobalX = gp.X; + _hoverGlobalY = gp.Y; + } + else + { + _hoverGlobalX = pos.X; + _hoverGlobalY = pos.Y; + } + + UpdateTooltip(col, row); + } + + private void OnMapPointerExited(object? sender, PointerEventArgs e) + { + HideTooltip(); + } + + private void HideTooltip() + { + _hoverCol = -1; + _hoverRow = -1; + TooltipService?.Hide(); + } + + private void UpdateTooltip(int col, int row) + { + var svc = TooltipService; + var result = _lastResult; + if (svc is null || result is null) { svc?.Hide(); return; } + + int bpp = result.Bpp; + int centerX = result.CenterX; + int centerY = result.CenterY; + + int topPixelY = row * 2; + int botPixelY = row * 2 + 1; + + int baseX = result.PlayerBlockX + (col - centerX) * bpp; + int baseZ_top = result.PlayerBlockZ + (topPixelY - centerY) * bpp; + int baseZ_bot = result.PlayerBlockZ + (botPixelY - centerY) * bpp; + + var lines = new List(); + + if (bpp == 1) + { + int surfY_top = (topPixelY < result.Heights.GetLength(1)) ? result.Heights[col, topPixelY] : 0; + int surfY_bot = (botPixelY < result.Heights.GetLength(1)) ? result.Heights[col, botPixelY] : 0; + + string coordLine = baseZ_top == baseZ_bot + ? $"{baseX}, {surfY_top}, {baseZ_top}" + : $"{baseX}, {surfY_top}, {baseZ_top} / {baseX}, {surfY_bot}, {baseZ_bot}"; + lines.Add(new TuiTooltipLine { Text = coordLine, Foreground = Brushes.White }); + + if (result.BlockTypes is not null) + { + var mat_top = result.BlockTypes[col, topPixelY]; + var mat_bot = (botPixelY < result.BlockTypes.GetLength(1)) + ? result.BlockTypes[col, botPixelY] : mat_top; + string blockLine = mat_top == mat_bot + ? FormatMaterialName(mat_top) + : $"{FormatMaterialName(mat_top)} / {FormatMaterialName(mat_bot)}"; + lines.Add(new TuiTooltipLine { Text = blockLine, Foreground = Brushes.LightGray }); + } + } + else + { + int endX = baseX + bpp - 1; + int endZ_bot = baseZ_bot + bpp - 1; + string coordLine = $"X {baseX}~{endX} Z {baseZ_top}~{endZ_bot}"; + lines.Add(new TuiTooltipLine { Text = coordLine, Foreground = Brushes.White }); + + AppendBlockSummaryLines(result, col, topPixelY, botPixelY, lines); + } + + AppendEntityInfoLines(result, col, topPixelY, botPixelY, lines); + + if (lines.Count == 0) + { + svc.Hide(); + return; + } + + bool preferRight = Position switch + { + MinimapPosition.top_left or MinimapPosition.bottom_left => true, + MinimapPosition.top_right or MinimapPosition.bottom_right => false, + _ => true, + }; + + double mx = _hoverGlobalX; + double my = _hoverGlobalY; + + if (Position == MinimapPosition.center + && this.VisualRoot is Visual root) + { + preferRight = mx < root.Bounds.Width / 2; + } + + svc.Show(mx, my, lines, preferRight); + } + + private void AppendBlockSummaryLines(SampleResult result, int col, int topPy, int botPy, + List lines) + { + if (result.BlockSummary is null) return; + + var merged = new Dictionary(); + MergeBlockCounts(result.BlockSummary, col, topPy, merged); + if (botPy < result.BlockSummary.GetLength(1)) + MergeBlockCounts(result.BlockSummary, col, botPy, merged); + + if (merged.Count == 0) return; + + var sorted = merged.OrderByDescending(kv => kv.Value).Take(4); + + var parts = new List(); + foreach (var kv in sorted) + { + if (kv.Key == Material.Air && merged.Count > 1) continue; + parts.Add(kv.Value > 1 + ? $"{FormatMaterialName(kv.Key)} x{kv.Value}" + : FormatMaterialName(kv.Key)); + } + + if (parts.Count == 0) return; + + lines.Add(new TuiTooltipLine + { + Text = string.Join(", ", parts), + Foreground = Brushes.LightGray, + }); + } + + private static void MergeBlockCounts(List<(Material Mat, int Count)>?[,] summary, + int px, int py, Dictionary target) + { + var list = summary[px, py]; + if (list is null) return; + foreach (var (mat, count) in list) + { + if (target.TryGetValue(mat, out int c)) + target[mat] = c + count; + else + target[mat] = count; + } + } + + private static void AppendEntityInfoLines(SampleResult result, int col, int topPy, int botPy, + List lines) + { + var entityMap = result.EntityMap; + if (entityMap is null) return; + + var combined = new List(); + AddEntitiesFromPixel(entityMap, col, topPy, combined); + if (botPy < entityMap.GetLength(1)) + AddEntitiesFromPixel(entityMap, col, botPy, combined); + + if (combined.Count == 0) return; + + combined.Sort((a, b) => b.Priority.CompareTo(a.Priority)); + int shown = 0; + var seen = new HashSet(); + foreach (var ent in combined) + { + if (shown >= 4) break; + string key = $"{ent.Name}_{ent.Health:F0}"; + if (!seen.Add(key)) continue; + + var catColor = MinimapEntityClassifier.GetBaseColor(ent.Category); + string coordStr = $"({ent.X:F1}, {ent.Y:F1}, {ent.Z:F1})"; + string hpStr = ""; + if (ent.Health > 0) + { + hpStr = ent.MaxHealth > 0 + ? $" HP:{ent.Health:F0}/{ent.MaxHealth:F0}" + : $" HP:{ent.Health:F0}"; + } + + lines.Add(new TuiTooltipLine + { + Text = $"{ent.Name} {coordStr}{hpStr}", + Foreground = new SolidColorBrush(catColor), + }); + shown++; + } + } + + private static void AddEntitiesFromPixel(List?[,] map, + int px, int py, List target) + { + if (px >= 0 && px < map.GetLength(0) && py >= 0 && py < map.GetLength(1)) + { + var list = map[px, py]; + if (list is not null) + target.AddRange(list); + } + } + + private static string FormatMaterialName(Material mat) + { + if (mat == Material.Air) return "Air"; + string raw = mat.ToString(); + return raw.Replace('_', ' '); + } + + private void UpdateInfoBarAndLegend(McClient client, int bpp, + HashSet categories, int mapW, bool caveModeActive) + { + var loc = client.GetCurrentLocation(); + float yaw = client.GetYaw(); + string arrow = GetDirectionArrow(yaw); + + int x = (int)Math.Floor(loc.X); + int y = (int)Math.Floor(loc.Y); + int z = (int)Math.Floor(loc.Z); + + string caveSuffix = caveModeActive ? " \u25bc" : ""; + string coordPart = $"{x}, {y}, {z} {arrow} {bpp}:1{caveSuffix}"; + + var legendParts = new List(); + var legendColors = new List(); + + var sorted = categories + .Where(c => c != MobCategory.NonLiving) + .OrderByDescending(MinimapEntityClassifier.GetPriority); + + int catCount = 0; + foreach (var cat in sorted) + { + if (catCount >= 4) break; + legendParts.Add(MinimapEntityClassifier.GetCategoryLabel(cat)); + legendColors.Add(MinimapEntityClassifier.GetBaseColor(cat)); + catCount++; + } + + int legendLen = 0; + for (int i = 0; i < legendParts.Count; i++) + legendLen += 1 + legendParts[i].Length + (i > 0 ? 1 : 0); + + bool fitsOnOneLine = legendParts.Count > 0 + && coordPart.Length + 2 + legendLen <= mapW; + + _infoRow.Children.Clear(); + _infoRow.Children.Add(new TextBlock + { + Text = coordPart, + Foreground = Brushes.Gray, + Padding = new Thickness(0), + }); + + if (fitsOnOneLine) + { + AppendLegendItems(_infoRow, legendParts, legendColors, leftMargin: 2); + _legendPanel.Children.Clear(); + _legendPanel.IsVisible = false; + } + else + { + _legendPanel.IsVisible = legendParts.Count > 0; + _legendPanel.Children.Clear(); + AppendLegendItems(_legendPanel, legendParts, legendColors, leftMargin: 0); + } + } + + private static void AppendLegendItems(StackPanel panel, + List parts, List colors, int leftMargin) + { + for (int i = 0; i < parts.Count; i++) + { + int ml = i == 0 ? leftMargin : 1; + panel.Children.Add(new TextBlock + { + Text = "\u25cf", + Foreground = new SolidColorBrush(colors[i]), + Padding = new Thickness(0), + Margin = ml > 0 ? new Thickness(ml, 0, 0, 0) : new Thickness(0), + }); + panel.Children.Add(new TextBlock + { + Text = parts[i], + Foreground = Brushes.Gray, + Padding = new Thickness(0), + Margin = new Thickness(0), + }); + } + } + + private static string GetDirectionArrow(float yaw) + { + double normalized = ((yaw % 360) + 360) % 360; + int index = (int)Math.Round(normalized / 45.0) % 8; + return index switch + { + 0 => "\u2193", // S + 1 => "\u2199", // SW + 2 => "\u2190", // W + 3 => "\u2196", // NW + 4 => "\u2191", // N + 5 => "\u2197", // NE + 6 => "\u2192", // E + 7 => "\u2198", // SE + _ => "\u2193", + }; + } + } +} diff --git a/MinecraftClient/Tui/MinimapEntityCategories.json b/MinecraftClient/Tui/MinimapEntityCategories.json new file mode 100644 index 00000000..c80b7c0b --- /dev/null +++ b/MinecraftClient/Tui/MinimapEntityCategories.json @@ -0,0 +1,167 @@ +{ + "version": "26.1-rc-2", + "hostile": [ + "Blaze", + "Bogged", + "Breeze", + "CamelHusk", + "Creaking", + "Creeper", + "Drowned", + "ElderGuardian", + "EnderDragon", + "Endermite", + "Evoker", + "Ghast", + "Giant", + "Guardian", + "Hoglin", + "Husk", + "Illusioner", + "MagmaCube", + "Parched", + "Phantom", + "Piglin", + "PiglinBrute", + "Pillager", + "Ravager", + "Shulker", + "Silverfish", + "Skeleton", + "Slime", + "Stray", + "Vex", + "Vindicator", + "Warden", + "Witch", + "Wither", + "WitherSkeleton", + "Zoglin", + "Zombie", + "ZombieNautilus", + "ZombieVillager" + ], + "passive": [ + "Allay", + "Armadillo", + "Axolotl", + "Bat", + "Camel", + "Cat", + "Chicken", + "Cod", + "Cow", + "Donkey", + "Fox", + "Frog", + "GlowSquid", + "HappyGhast", + "Horse", + "Mooshroom", + "Mule", + "Nautilus", + "Ocelot", + "Parrot", + "Pig", + "Pufferfish", + "Rabbit", + "Salmon", + "Sheep", + "SkeletonHorse", + "Sniffer", + "Squid", + "Strider", + "Tadpole", + "TropicalFish", + "Turtle", + "Villager", + "WanderingTrader", + "ZombieHorse" + ], + "neutral": [ + "Bee", + "CaveSpider", + "CopperGolem", + "Dolphin", + "Enderman", + "Goat", + "IronGolem", + "Llama", + "Panda", + "PolarBear", + "SnowGolem", + "Spider", + "TraderLlama", + "Wolf", + "ZombifiedPiglin" + ], + "non_living": [ + "AcaciaBoat", + "AcaciaChestBoat", + "AreaEffectCloud", + "ArmorStand", + "Arrow", + "BambooChestRaft", + "BambooRaft", + "BirchBoat", + "BirchChestBoat", + "BlockDisplay", + "BreezeWindCharge", + "CherryBoat", + "CherryChestBoat", + "ChestMinecart", + "CommandBlockMinecart", + "DarkOakBoat", + "DarkOakChestBoat", + "DragonFireball", + "Egg", + "EndCrystal", + "EnderPearl", + "EvokerFangs", + "ExperienceBottle", + "ExperienceOrb", + "EyeOfEnder", + "FallingBlock", + "Fireball", + "FireworkRocket", + "FishingBobber", + "FurnaceMinecart", + "GlowItemFrame", + "HopperMinecart", + "Interaction", + "Item", + "ItemDisplay", + "ItemFrame", + "JungleBoat", + "JungleChestBoat", + "LeashKnot", + "LightningBolt", + "LingeringPotion", + "LlamaSpit", + "MangroveBoat", + "MangroveChestBoat", + "Mannequin", + "Marker", + "Minecart", + "OakBoat", + "OakChestBoat", + "OminousItemSpawner", + "Painting", + "PaleOakBoat", + "PaleOakChestBoat", + "ShulkerBullet", + "SmallFireball", + "Snowball", + "SpawnerMinecart", + "SpectralArrow", + "SplashPotion", + "SpruceBoat", + "SpruceChestBoat", + "TextDisplay", + "Tnt", + "TntMinecart", + "Trident", + "WindCharge", + "WitherSkull" + ] +} \ No newline at end of file diff --git a/MinecraftClient/Tui/MinimapEntityClassifier.cs b/MinecraftClient/Tui/MinimapEntityClassifier.cs new file mode 100644 index 00000000..2daf1ea6 --- /dev/null +++ b/MinecraftClient/Tui/MinimapEntityClassifier.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using Avalonia.Media; +using MinecraftClient.Mapping; + +namespace MinecraftClient.Tui +{ + public enum MobCategory + { + Hostile, + Passive, + Neutral, + Player, + NonLiving, + } + + public enum MinimapPosition + { + top_left, + top_right, + center, + bottom_left, + bottom_right, + } + + public sealed class NameDisplayConfig + { + public volatile bool Players = false; + public volatile bool Hostile = false; + public volatile bool Neutral = false; + public volatile bool Passive = false; + + public bool AnyEnabled => Players || Hostile || Neutral || Passive; + + public void SetAll(bool value) + { + Players = value; + Hostile = value; + Neutral = value; + Passive = value; + } + + public bool ShouldShowName(MobCategory category) => category switch + { + MobCategory.Player => Players, + MobCategory.Hostile => Hostile, + MobCategory.Neutral => Neutral, + MobCategory.Passive => Passive, + _ => false, + }; + } + + /// + /// Classifies entities into minimap categories using data extracted from + /// Minecraft's MobCategory assignments. Categories are loaded from the + /// embedded MinimapEntityCategories.json resource generated by + /// tools/gen_entity_category_map.py. + /// + public static class MinimapEntityClassifier + { + public static readonly Color HostileColor = Color.FromRgb(255, 68, 68); + public static readonly Color PassiveColor = Color.FromRgb(68, 255, 68); + public static readonly Color NeutralColor = Color.FromRgb(255, 170, 0); + public static readonly Color PlayerColor = Color.FromRgb(255, 255, 255); + public static readonly Color FadedGray = Color.FromRgb(100, 100, 100); + + private static readonly FrozenDictionary CategoryTable; + + static MinimapEntityClassifier() + { + var table = new Dictionary(); + + try + { + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream("MinimapEntityCategories.json"); + if (stream is not null) + { + using var doc = JsonDocument.Parse(stream); + var root = doc.RootElement; + + LoadCategory(root, "hostile", MobCategory.Hostile, table); + LoadCategory(root, "passive", MobCategory.Passive, table); + LoadCategory(root, "neutral", MobCategory.Neutral, table); + LoadCategory(root, "non_living", MobCategory.NonLiving, table); + } + } + catch (Exception ex) + { + ConsoleIO.WriteLogLine($"[Minimap] Failed to load entity categories: {ex.Message}"); + } + + CategoryTable = table.ToFrozenDictionary(); + } + + private static void LoadCategory(JsonElement root, string key, + MobCategory category, Dictionary table) + { + if (!root.TryGetProperty(key, out var arr)) + return; + + foreach (var el in arr.EnumerateArray()) + { + var name = el.GetString(); + if (name is not null && Enum.TryParse(name, out var et)) + table.TryAdd(et, category); + } + } + + public static MobCategory Classify(EntityType type) + { + if (type == EntityType.Player) + return MobCategory.Player; + return CategoryTable.GetValueOrDefault(type, MobCategory.NonLiving); + } + + public static Color GetBaseColor(MobCategory category) => category switch + { + MobCategory.Hostile => HostileColor, + MobCategory.Passive => PassiveColor, + MobCategory.Neutral => NeutralColor, + MobCategory.Player => PlayerColor, + _ => FadedGray, + }; + + public static Color ApplyDepthFade(Color baseColor, double playerY, double entityY) + { + double depth = playerY - entityY; + + if (depth <= 5.0) + return baseColor; + + if (depth >= 15.0) + return FadedGray; + + double t = (depth - 5.0) / 10.0; + return Lerp(baseColor, FadedGray, t); + } + + public static bool ShouldDisplay(MobCategory category, double playerY, double entityY) + { + if (category == MobCategory.Player) + return true; + if (entityY >= playerY) + return true; + return playerY - entityY <= 15.0; + } + + public static int GetPriority(MobCategory category) => category switch + { + MobCategory.Hostile => 4, + MobCategory.Player => 3, + MobCategory.Neutral => 2, + MobCategory.Passive => 1, + _ => 0, + }; + + public static string GetCategoryLabel(MobCategory category) => category switch + { + MobCategory.Hostile => Translations.tui_minimap_legend_hostile, + MobCategory.Passive => Translations.tui_minimap_legend_passive, + MobCategory.Neutral => Translations.tui_minimap_legend_neutral, + MobCategory.Player => Translations.tui_minimap_legend_player, + _ => "?", + }; + + private static Color Lerp(Color a, Color b, double t) + { + byte r = (byte)(a.R + (b.R - a.R) * t); + byte g = (byte)(a.G + (b.G - a.G) * t); + byte bl = (byte)(a.B + (b.B - a.B) * t); + return Color.FromRgb(r, g, bl); + } + } +} diff --git a/MinecraftClient/Tui/ServerStatusPanelBuilder.cs b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs new file mode 100644 index 00000000..2c8daf6f --- /dev/null +++ b/MinecraftClient/Tui/ServerStatusPanelBuilder.cs @@ -0,0 +1,196 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Layout; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + internal static class ServerStatusPanelBuilder + { + private const int MaxSamplePlayers = 10; + private const int FaviconDisplaySize = 16; + + internal static Border Build(Protocol.ServerStatusInfo info) + { + var contentPanel = new DockPanel { Background = Brushes.Black }; + + if (info.FaviconBase64 is not null) + { + var iconGrid = BuildFaviconGrid(info.FaviconBase64, FaviconDisplaySize); + iconGrid.VerticalAlignment = VerticalAlignment.Center; + DockPanel.SetDock(iconGrid, Dock.Left); + contentPanel.Children.Add(iconGrid); + } + + var infoPanel = new StackPanel + { + Orientation = Orientation.Vertical, + Margin = new Thickness(1, 0, 0, 0), + VerticalAlignment = VerticalAlignment.Center, + }; + + AddMotd(infoPanel, info); + AddAddress(infoPanel, info); + AddVersion(infoPanel, info); + AddConnectingAs(infoPanel, info); + AddPing(infoPanel, info); + AddPlayers(infoPanel, info); + AddSamplePlayers(infoPanel, info); + + contentPanel.Children.Add(infoPanel); + + return new Border + { + BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)), + BorderThickness = new Thickness(1), + Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)), + Padding = new Thickness(1, 0), + Child = contentPanel, + Margin = new Thickness(0), + }; + } + + private static void AddMotd(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (string.IsNullOrEmpty(info.MotdRaw)) + return; + + try + { + string motdFormatted = Protocol.Message.ChatParser.ParseText(info.MotdRaw); + foreach (string line in motdFormatted.Split('\n')) + panel.Children.Add(McColorParser.CreateColoredTextBlock(line, TextWrapping.NoWrap)); + } + catch + { + panel.Children.Add(new TextBlock + { + Text = info.MotdRaw, + Foreground = Brushes.White, + TextWrapping = TextWrapping.NoWrap, + }); + } + } + + private static void AddAddress(StackPanel panel, Protocol.ServerStatusInfo info) + { + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_server)); + row.Inlines.Add(Value(info.Host, McColors.Aqua)); + row.Inlines.Add(new Run($":{info.Port}") { Foreground = McColors.Gray }); + panel.Children.Add(row); + } + + private static void AddVersion(StackPanel panel, Protocol.ServerStatusInfo info) + { + string versionClean = Scripting.ChatBot.GetVerbatim(info.VersionName); + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_version)); + row.Inlines.Add(Value(versionClean, McColors.Aqua)); + row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ProtocolVersion)) + { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(")") { Foreground = McColors.Gray }); + panel.Children.Add(row); + } + + private static void AddConnectingAs(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (info.ResolvedProtocol == 0) + return; + + string resolvedMcVer = Protocol.ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol); + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_connecting_as)); + row.Inlines.Add(Value(resolvedMcVer, McColors.Green)); + row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ResolvedProtocol)) + { Foreground = McColors.Gray }); + row.Inlines.Add(new Run(")") { Foreground = McColors.Gray }); + panel.Children.Add(row); + } + + private static void AddPing(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (info.PingMs < 0) + return; + + var pingColor = info.PingMs < 100 + ? McColors.Green + : info.PingMs < 300 + ? McColors.Yellow + : McColors.Red; + + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_ping)); + row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs)) + { Foreground = pingColor }); + panel.Children.Add(row); + } + + private static void AddPlayers(StackPanel panel, Protocol.ServerStatusInfo info) + { + var row = new TextBlock(); + row.Inlines!.Add(Label(Translations.mcc_server_info_label_players)); + row.Inlines.Add(Value($"{info.OnlinePlayers}", McColors.Green)); + row.Inlines.Add(new Run("/") { Foreground = McColors.Gray }); + row.Inlines.Add(Value($"{info.MaxPlayers}", McColors.Red)); + panel.Children.Add(row); + } + + private static void AddSamplePlayers(StackPanel panel, Protocol.ServerStatusInfo info) + { + if (info.SamplePlayers.Count == 0) + return; + + panel.Children.Add(new TextBlock + { + Text = Translations.mcc_server_info_label_online, + Foreground = McColors.Gray, + }); + + int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers); + for (int i = 0; i < shown; i++) + { + string name = info.SamplePlayers[i].Name; + if (name.Contains('\u00a7')) + panel.Children.Add(McColorParser.CreateColoredTextBlock($" {name}", TextWrapping.NoWrap)); + else + panel.Children.Add(new TextBlock + { + Text = $" {name}", + Foreground = McColors.Green, + }); + } + + if (info.SamplePlayers.Count > shown) + { + panel.Children.Add(new TextBlock + { + Text = $" {string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}", + Foreground = McColors.Gray, + }); + } + } + + private static Run Label(string text) => + new(text + " ") { Foreground = McColors.Gray }; + + private static Run Value(string text, IBrush color) => + new(text) { Foreground = color }; + + private static Grid BuildFaviconGrid(string base64Png, int displaySize) => + IconGridBuilder.BuildFromBase64(base64Png, displaySize); + + private static class McColors + { + public static readonly IBrush Gray = new SolidColorBrush(Color.FromRgb(170, 170, 170)); + public static readonly IBrush Aqua = new SolidColorBrush(Color.FromRgb(85, 255, 255)); + public static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(85, 255, 85)); + public static readonly IBrush Red = new SolidColorBrush(Color.FromRgb(255, 85, 85)); + public static readonly IBrush Yellow = new SolidColorBrush(Color.FromRgb(255, 255, 85)); + } + } +} diff --git a/MinecraftClient/Tui/TuiTooltipService.cs b/MinecraftClient/Tui/TuiTooltipService.cs new file mode 100644 index 00000000..0ae51607 --- /dev/null +++ b/MinecraftClient/Tui/TuiTooltipService.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace MinecraftClient.Tui +{ + public sealed class TuiTooltipLine + { + public string Text { get; init; } = ""; + public IBrush Foreground { get; init; } = Brushes.White; + } + + /// + /// Global tooltip that floats above all TUI content. + /// Owned by MainTuiView, used by minimap / chat / other components. + /// + public sealed class TuiTooltipService + { + private readonly Panel _rootPanel; + private readonly Canvas _canvas; + private readonly Border _border; + private readonly StackPanel _content; + + internal TuiTooltipService(Panel rootPanel) + { + _content = new StackPanel { Orientation = Avalonia.Layout.Orientation.Vertical }; + _border = new Border + { + Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)), + BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)), + BorderThickness = new Thickness(1), + Padding = new Thickness(1), + Child = _content, + IsVisible = false, + }; + + _canvas = new Canvas + { + IsHitTestVisible = false, + Children = { _border }, + }; + + _rootPanel = rootPanel; + rootPanel.Children.Add(_canvas); + } + + /// Global X of the mouse cursor. + /// Global Y of the mouse cursor. + /// + /// If true, try placing tooltip to the right of mouseX; + /// if false, try placing to the left. + /// The service auto-flips when the tooltip would overflow the screen. + /// + public void Show(double mouseX, double mouseY, IReadOnlyList lines, + bool preferRight = true) + { + _content.Children.Clear(); + + if (lines.Count == 0) + { + _border.IsVisible = false; + return; + } + + int maxChars = 0; + foreach (var line in lines) + { + _content.Children.Add(new TextBlock + { + Text = line.Text, + Foreground = line.Foreground, + TextWrapping = TextWrapping.Wrap, + Padding = new Thickness(0), + Margin = new Thickness(0), + FontSize = 1, + }); + if (line.Text.Length > maxChars) + maxChars = line.Text.Length; + } + + double tipW = maxChars + 4; + double screenW = _rootPanel.Bounds.Width; + + const double gap = 1; + double gx; + if (preferRight) + { + gx = mouseX + gap; + if (gx + tipW > screenW) + gx = mouseX - tipW - gap; + } + else + { + gx = mouseX - tipW - gap; + if (gx < 0) + gx = mouseX + gap; + } + + Canvas.SetLeft(_border, Math.Max(0, gx)); + Canvas.SetTop(_border, Math.Max(0, mouseY)); + _border.IsVisible = true; + } + + public void Hide() + { + _border.IsVisible = false; + _content.Children.Clear(); + } + + public bool IsVisible => _border.IsVisible; + } +} diff --git a/MinecraftClient/config/sample-script-packet-capture.cs b/MinecraftClient/config/sample-script-packet-capture.cs index 981ddb49..aa8b15ff 100644 --- a/MinecraftClient/config/sample-script-packet-capture.cs +++ b/MinecraftClient/config/sample-script-packet-capture.cs @@ -4,8 +4,6 @@ MCC.LoadBot(new PacketCadenceCaptureBot()); //MCCScript Extensions -using System.Threading; - public class PacketCadenceCaptureBot : ChatBot { private const int CaptureDurationSeconds = 5; diff --git a/README.md b/README.md index fb690dd9..76228060 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,25 @@ ## Download -Get development builds from the [Releases section](https://github.com/MCCTeam/Minecraft-Console-Client/releases/latest) +Get the latest release from the [Releases section](https://github.com/MCCTeam/Minecraft-Console-Client/releases/latest). + +## Quick Install ⚡ + +Open a terminal in the folder where you want MCC and run: + +Linux / macOS: + +```bash +curl -fsSL https://mccteam.github.io/install.sh | sh +``` + +Windows (PowerShell): + +```powershell +iwr -useb https://mccteam.github.io/install.ps1 | iex +``` + +The script detects your architecture and downloads the right binary. For more options (including `wget` and manual downloads), see the [installation guide](https://mccteam.github.io/guide/installation.html). ## How to use 📚 diff --git a/crowdin.yml b/crowdin.yml index 5e80b8bb..89813ca1 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,6 +1,6 @@ "project_id_env": "CROWDIN_PROJECT_ID" "api_token_env": "CROWDIN_PERSONAL_TOKEN" -"base_path": "/" +"base_path": "./" "preserve_hierarchy": true "base_url": "https://api.crowdin.com" diff --git a/docs/.vuepress/public/install.ps1 b/docs/.vuepress/public/install.ps1 new file mode 100644 index 00000000..31f2e324 --- /dev/null +++ b/docs/.vuepress/public/install.ps1 @@ -0,0 +1,85 @@ +# Minecraft Console Client - Installer for Windows +# Downloads the latest MinecraftClient binary for your Windows architecture. +# Usage (PowerShell): iwr -useb https://mccteam.github.io/install.ps1 | iex + +$ErrorActionPreference = 'Stop' + +$REPO = "MCCTeam/Minecraft-Console-Client" +$OUTPUT = "MinecraftClient.exe" + +# --- Detect CPU architecture --- +$arch = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture +$archId = switch ($arch) { + 'X64' { 'x64' } + 'X86' { 'x86' } + 'Arm64' { 'arm64' } + default { + Write-Error "Unsupported CPU architecture: $arch" + exit 1 + } +} + +$suffix = "win-$archId" + +# --- Fetch latest release metadata from GitHub API --- +$apiUrl = "https://api.github.com/repos/$REPO/releases/latest" +Write-Host "Fetching latest release information..." +$release = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing + +# --- Locate the correct asset --- +$asset = $release.assets | Where-Object { $_.name -match "^MinecraftClient-.*-$([regex]::Escape($suffix))\.exe$" } | Select-Object -First 1 + +if (-not $asset) { + Write-Error "Could not find a release asset for '$suffix'." + exit 1 +} + +$downloadUrl = $asset.browser_download_url +$tag = $release.tag_name + +Write-Host "Downloading MinecraftClient $tag ($suffix)..." + +# Download with a built-in ASCII progress bar (no external tools required). +# HttpWebRequest streams the body on the main thread so we can update the +# progress bar inline without any Runspace or thread-safety concerns. +$outPath = Join-Path (Get-Location).Path $OUTPUT +$request = [System.Net.HttpWebRequest]::Create($downloadUrl) +$response = $request.GetResponse() +$totalBytes = $response.ContentLength + +$responseStream = $response.GetResponseStream() +$fileStream = [System.IO.File]::Create($outPath) +$buffer = New-Object byte[] 32768 +$totalRead = 0 + +try { + while ($true) { + $read = $responseStream.Read($buffer, 0, $buffer.Length) + if ($read -le 0) { break } + $fileStream.Write($buffer, 0, $read) + $totalRead += $read + if ($totalBytes -gt 0) { + $pct = [int]($totalRead * 100 / $totalBytes) + $filled = '=' * [int]($pct / 2) + $bar = $filled.PadRight(50) + $recv = [math]::Round($totalRead / 1MB, 1) + $total = [math]::Round($totalBytes / 1MB, 1) + # Use [Console]::Write with an explicit \r so the cursor returns to + # column 0 and overwrites the previous bar. Write-Host -NoNewline + # does not reliably reposition the cursor when the script is run + # via iex (pipe mode), producing multiple bars on one line. + $line = "`r[{0}] {1,3}% {2,6:N1} / {3,6:N1} MB" -f $bar, $pct, $recv, $total + [Console]::Write($line) + } + } +} finally { + $fileStream.Close() + $responseStream.Close() + $response.Close() +} + +[Console]::WriteLine() # end the progress line + +Write-Host "" +Write-Host "Downloaded: .\$OUTPUT" +Write-Host "Run with: .\$OUTPUT --help" diff --git a/docs/.vuepress/public/install.sh b/docs/.vuepress/public/install.sh new file mode 100644 index 00000000..d3e74d3d --- /dev/null +++ b/docs/.vuepress/public/install.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# Minecraft Console Client - Installer +# Downloads the latest MinecraftClient binary for your Linux or macOS platform. +# Usage: curl -fsSL https://mccteam.github.io/install.sh | sh +# or: wget -qO- https://mccteam.github.io/install.sh | sh + +set -e + +REPO="MCCTeam/Minecraft-Console-Client" +OUTPUT="MinecraftClient" + +# --- Detect OS --- +OS=$(uname -s) +case "$OS" in + Linux) PLATFORM="linux" ;; + Darwin) PLATFORM="osx" ;; + *) + echo "Error: Unsupported OS '$OS'. This script supports Linux and macOS." >&2 + exit 1 + ;; +esac + +# --- Detect CPU architecture --- +ARCH=$(uname -m) +case "$ARCH" in + x86_64|amd64) ARCH_ID="x64" ;; + aarch64|arm64) ARCH_ID="arm64" ;; + armv7l|armv8l|armhf) ARCH_ID="arm" ;; + arm*) ARCH_ID="arm" ;; + *) + echo "Error: Unsupported CPU architecture '$ARCH'." >&2 + exit 1 + ;; +esac + +# macOS does not have an arm (32-bit) build +if [ "$PLATFORM" = "osx" ] && [ "$ARCH_ID" = "arm" ]; then + echo "Error: 32-bit ARM is not supported on macOS." >&2 + exit 1 +fi + +SUFFIX="${PLATFORM}-${ARCH_ID}" + +# --- Download helpers: prefer curl, fall back to wget --- +_download_stdout() { + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$1" + elif command -v wget >/dev/null 2>&1; then + wget -qO- "$1" + else + echo "Error: Neither 'curl' nor 'wget' is available. Please install one and retry." >&2 + exit 1 + fi +} + +_download_file() { + if command -v curl >/dev/null 2>&1; then + curl -fL --progress-bar -o "$2" "$1" + elif command -v wget >/dev/null 2>&1; then + # --show-progress forces the progress bar even when stdout is not a TTY. + # Fall back silently to default output if the flag is not supported + # (older wget versions, e.g. BusyBox wget). + if wget --show-progress -O "$2" "$1" 2>/dev/null; then + return 0 + fi + wget -O "$2" "$1" + else + echo "Error: Neither 'curl' nor 'wget' is available. Please install one and retry." >&2 + exit 1 + fi +} + +# --- Fetch latest release metadata from GitHub API --- +API_URL="https://api.github.com/repos/${REPO}/releases/latest" +echo "Fetching latest release information..." +RELEASE_JSON=$(_download_stdout "$API_URL") + +# --- Parse asset download URL (no external tools required) --- +# The JSON key "browser_download_url" appears once per asset. +# We match the key followed by the URL, anchoring on the platform-arch suffix +# and the closing quote so that e.g. "linux-arm" does not match "linux-arm64". +# The ' *: *' pattern handles optional spaces around the colon (GitHub API adds spaces). +ASSET_URL=$(printf '%s' "$RELEASE_JSON" \ + | grep -o '"browser_download_url" *: *"[^"]*-'"${SUFFIX}"'"' \ + | grep -o 'https://[^"]*' \ + | head -1) + +if [ -z "$ASSET_URL" ]; then + echo "Error: Could not find a release asset for platform '${SUFFIX}'." >&2 + exit 1 +fi + +# --- Extract tag name for display --- +TAG=$(printf '%s' "$RELEASE_JSON" \ + | grep -o '"tag_name" *: *"[^"]*"' \ + | head -1 \ + | grep -o '"[^"]*"$' \ + | tr -d '"') + +echo "Downloading MinecraftClient ${TAG} (${SUFFIX})..." +_download_file "$ASSET_URL" "$OUTPUT" +chmod +x "$OUTPUT" + +echo "" +echo "Downloaded: ./${OUTPUT}" +echo "Run with: ./${OUTPUT} --help" diff --git a/docs/guide/chat-bots.md b/docs/guide/chat-bots.md index 512f1664..3c725a7a 100644 --- a/docs/guide/chat-bots.md +++ b/docs/guide/chat-bots.md @@ -748,6 +748,46 @@ redirectFrom: - **Default:** `3.0` + #### `Auto_Tool_Switch` + + - **Description:** + + Automatically switch to a more suitable tool from your inventory before digging. + + When `Durability_Limit` is above zero, tools below that durability threshold are skipped. + + - **Available values:** `true` and `false` + + - **Type:** `boolean` + + - **Default:** `false` + + #### `Durability_Limit` + + - **Description:** + + Will not use tools with less durability than this. + + Set to `0` to disable this durability check. + + - **Type:** `integer` + + - **Default:** `2` + + #### `Drop_Low_Durability_Tools` + + - **Description:** + + Drop the replaced tool if its remaining durability is below `Durability_Limit`. + + This setting is only useful when `Auto_Tool_Switch` is enabled. + + - **Available values:** `true` and `false` + + - **Type:** `boolean` + + - **Default:** `false` + #### `Dig_Timeout` - **Description:** @@ -927,6 +967,7 @@ redirectFrom: - **Description:** Automatically catch fish using a fishing rod. + Bite detection combines bobber movement, bobber velocity, and splash sounds.

Note

@@ -1103,6 +1144,66 @@ redirectFrom: - **Default:** `0.2` + #### `Enable_Velocity_Detection` + + - **Description:** + + Enables bite detection using the fishing bobber velocity packet. + + This improves reliability when bobber X/Z movement is constrained (for example by blocks near the water surface). + + - **Available values:** `true` and `false`. + + - **Type:** `boolean` + + - **Default:** `true` + + #### `Velocity_Hook_Threshold` + + - **Description:** + + Velocity Y threshold in blocks/tick for velocity-based bite detection. + + Values below this threshold are considered a bite. Keep this value negative. + + - **Type:** `float` + + - **Default:** `-0.2` + + #### `Enable_Sound_Detection` + + - **Description:** + + Enables bite detection using nearby splash sounds (`entity.fishing_bobber.splash`). + + - **Available values:** `true` and `false`. + + - **Type:** `boolean` + + - **Default:** `true` + + #### `Sound_Distance` + + - **Description:** + + Maximum distance in blocks between a splash sound and the tracked bobber to treat it as a bite. + + - **Type:** `float` + + - **Default:** `5.0` + + #### `Detection_Warmup` + + - **Description:** + + Delay in seconds after bobber spawn before bite detection starts. + + This helps ignore the initial cast-entry splash/motion. + + - **Type:** `float` + + - **Default:** `1.0` + #### `Log_Fish_Bobber` - **Description:** diff --git a/docs/guide/creating-bots.md b/docs/guide/creating-bots.md index e374f273..076f4600 100644 --- a/docs/guide/creating-bots.md +++ b/docs/guide/creating-bots.md @@ -229,6 +229,120 @@ Make a built-in MCC chat bot named AutoTorch and wire it fully into the repo con Create a standalone MCC /script bot that follows private messages, uses GetVerbatim(text), and replies only to bot owners. Use the mcc-chatbot-authoring skill. ``` +## Achievements And Advancements + +Chat bots and C# scripts can read the current achievement state and react to updates. + +Useful methods: + +- `GetAchievements()` +- `GetUnlockedAchievements()` +- `GetLockedAchievements()` +- `OnAchievementUpdate(IReadOnlyList updated, IReadOnlyList removedIds, bool reset)` + +Things worth knowing: + +- On `1.8` to `1.11.2`, ids use the legacy `achievement.*` format. +- On `1.12+`, ids use advancement resource ids such as `minecraft:story/root`. +- Legacy achievements usually have `Title = null` and `Description = null` because the server does not send display metadata in the statistics packet. +- On newer versions, revoking an advancement may remove it from the current set instead of turning it into a locked entry, so `removedIds` matters. + +Example: + +```csharp +//MCCScript 1.0 + +MCC.LoadBot(new AchievementWatcher()); + +//MCCScript Extensions + +public class AchievementWatcher : ChatBot +{ + public override void AfterGameJoined() + { + Achievement[] known = GetAchievements(); + LogToConsole($"Known achievements: {known.Length}"); + } + + public override void OnAchievementUpdate(IReadOnlyList updated, IReadOnlyList removedIds, bool reset) + { + LogToConsole($"Achievement update: reset={reset}, updated={updated.Count}, removed={removedIds.Count}"); + + foreach (Achievement achievement in updated) + { + string title = achievement.Title ?? achievement.Id; + string state = achievement.IsCompleted ? "done" : "todo"; + LogToConsole($" - {title}: {state}"); + } + + foreach (string removedId in removedIds) + LogToConsole($" - removed: {removedId}"); + } +} +``` + +## Scoreboard teams + +Chat bots and C# scripts can read the current team state and react to team changes. + +Useful methods and events: + +- `GetTeams()` - returns a snapshot of all teams the server has sent +- `GetPlayerTeam(playerName)` - returns the team a specific player is on, or `null` +- `OnTeam(teamName, method, displayName, friendlyFlags, nameTagVisibility, collisionRule, color, prefix, suffix, players)` - called whenever a team packet arrives + +The `method` byte tells you what changed: + +- `0` - team created (includes full parameters and initial member list) +- `1` - team removed +- `2` - team parameters updated (display name, colors, rules) +- `3` - players added to the team +- `4` - players removed from the team + +The `color` field is a `ChatFormatting` enum ordinal. Common values: `0`=black, `9`=blue, `10`=green, `12`=red, `14`=yellow, `-1`=none/reset. + +The `nameTagVisibility` and `collisionRule` strings take values from the Minecraft wiki: `"always"`, `"never"`, `"hideForOtherTeams"`, `"hideForOwnTeam"` (visibility) or `"pushOtherTeams"`, `"pushOwnTeam"` (collision). + +Example: + +```csharp +//MCCScript 1.0 + +MCC.LoadBot(new TeamWatcher()); + +//MCCScript Extensions + +public class TeamWatcher : ChatBot +{ + public override void AfterGameJoined() + { + foreach (var team in GetTeams().Values) + LogToConsole($"Team '{team.Name}' has {team.Members.Count} member(s)"); + } + + public override void OnTeam(string teamName, byte method, string displayName, + byte friendlyFlags, string nameTagVisibility, string collisionRule, + int color, string prefix, string suffix, List players) + { + switch (method) + { + case 0: + LogToConsole($"Team '{teamName}' created with {players.Count} member(s)"); + break; + case 1: + LogToConsole($"Team '{teamName}' removed"); + break; + case 3: + LogToConsole($"{string.Join(", ", players)} joined team '{teamName}'"); + break; + case 4: + LogToConsole($"{string.Join(", ", players)} left team '{teamName}'"); + break; + } + } +} +``` + ## C# API The authoritative reference for the C# API is [ChatBot.cs](https://github.com/MCCTeam/Minecraft-Console-Client/blob/master/MinecraftClient/Scripting/ChatBot.cs). diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 612cf37c..1ba686ed 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -4,6 +4,7 @@ title: Installation # Installation +- [Quick Install (one-liner)](#quick-install) - [YouTube Tutorials](#youtube-tutorials) - [Download a compiled binary](#download-a-compiled-binary) - [Building from the source code](#building-from-the-source-code) @@ -11,6 +12,42 @@ title: Installation - [Run on Android](#run-on-android) - [Run MCC 24/7 on a VPS](#run-on-a-vps) +## Quick Install + +The quickest way to get MCC is to run the installer script for your platform. It auto-detects your OS and CPU architecture, fetches the latest release from GitHub, and saves the binary to your current directory. + +### Linux / macOS + +Open a terminal in the folder where you want MCC and run: + +```bash +curl -fsSL https://mccteam.github.io/install.sh | sh +``` + +If you prefer `wget`: + +```bash +wget -qO- https://mccteam.github.io/install.sh | sh +``` + +The script downloads `MinecraftClient` and marks it executable. Supported architectures: `x64`, `arm64`, `arm` (Linux only). + +### Windows + +Open **PowerShell** in the folder where you want MCC and run: + +```powershell +iwr -useb https://mccteam.github.io/install.ps1 | iex +``` + +The script downloads `MinecraftClient.exe`. Supported architectures: `x64`, `x86`, `arm64`. + +::: tip +You can also download the scripts directly and inspect them before running: +- Linux/macOS: [install.sh](https://mccteam.github.io/install.sh) +- Windows: [install.ps1](https://mccteam.github.io/install.ps1) +::: + ## YouTube Tutorials If you're not the kind of person that likes textual tutorials, our community has made video tutorials available on YouTube. diff --git a/docs/guide/usage.md b/docs/guide/usage.md index 833d5dd3..24478acb 100644 --- a/docs/guide/usage.md +++ b/docs/guide/usage.md @@ -219,6 +219,54 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q +
+achievement + +- **Description:** + + Show the achievements or advancements currently known to MCC. + + On Minecraft `1.8` to `1.11.2`, MCC tracks legacy achievements such as `achievement.openInventory`. + + On Minecraft `1.12+`, MCC tracks advancements such as `minecraft:story/root`. + +- **Usage:** + + ``` + /achievement + /achievement list + /achievement locked + /achievement unlocked + ``` + +- **Examples:** + + List everything MCC currently knows: + + ``` + /achievement + ``` + + Show only incomplete entries: + + ``` + /achievement locked + ``` + + Show only completed entries: + + ``` + /achievement unlocked + ``` + +- **Notes:** + + The command only shows data the server has already sent to MCC. + + Legacy achievements do not include titles or descriptions in the protocol, so older servers usually show the raw id instead. + +
+
bed @@ -650,6 +698,79 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+recipebook + +- **Description:** + + List unlocked recipe book entries and ask the server to place one of them into the active crafting inventory. + +

Note

+ + **You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this command to work.** + +
+ +

Note

+ + **`craft` and `craftall` need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.** + +
+ +

Warning

+ + **Recipe book crafting is supported on Minecraft `1.13+`.** + +
+ + `list` shows the recipe book entries MCC is currently tracking. + + On newer versions, the list can contain numeric display ids instead of plain recipe names. If you see something like `838: Oak Planks`, use `838` with `craft` or `craftall`. + + `craft` and `craftall` send a recipe-book request to the server. They do not automatically take the result item for you. After the recipe appears in the active inventory, take the output slot the same way you would handle any other inventory action. + +- **Usage:** + + ``` + /recipebook list + ``` + + ``` + /recipebook craft + ``` + + ``` + /recipebook craftall + ``` + +- **Examples:** + + Show the currently tracked recipe book entries: + + ``` + /recipebook list + ``` + + Request one recipe placement: + + ``` + /recipebook craft minecraft:oak_planks + ``` + + On newer versions, use the numeric id shown by `/recipebook list`: + + ``` + /recipebook craftall 838 + ``` + + If the recipe is placed in the player crafting grid, take the result from slot `0`: + + ``` + /inventory player click 0 + ``` + +
+
connect @@ -832,6 +953,30 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
+
+teams + +- **Description:** + + List all scoreboard teams the server has sent, along with their members and settings. + +- **Usage:** + + ``` + /teams + ``` + +- **Example output:** + + ``` + Team 'RedTeam' (display: RedTeam, color: 12, prefix: '', suffix: '', nameTagVisibility: always, collisionRule: always, friendlyFire: True, seeInvisibles: True) + Members (2): Steve, Alex + Team 'BlueTeam' (display: BlueTeam, color: 9, prefix: '', suffix: '', nameTagVisibility: always, collisionRule: always, friendlyFire: True, seeInvisibles: True) + No members. + ``` + +
+
useitem diff --git a/tools/README.md b/tools/README.md index 4dceae9c..7d33d2cf 100644 --- a/tools/README.md +++ b/tools/README.md @@ -135,6 +135,44 @@ Data source: `https://raw.githubusercontent.com/PrismarineJS/minecraft-data/mast Uses `curl` with resume (`-C -`) for reliable download over slow connections. Falls back to manual download if retries are exhausted. +## gen_block_color_map.py -- Generate minimap block color JSON + +Extracts block-to-MapColor RGB mappings from decompiled Minecraft source for the TUI minimap. + +```bash +python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled +# -> MinecraftClient/Tui/MinimapBlockColors.json +``` + +Parses three files from the decompiled source: +- `MapColor.java` -- extracts the 64 base MapColor constants and their RGB values +- `DyeColor.java` -- maps dye colors to MapColor constants +- `Blocks.java` -- determines each block's assigned MapColor via `.mapColor()` calls + +Output: `MinecraftClient/Tui/MinimapBlockColors.json` (embedded as a resource via `.csproj`). Contains color entries, plus lists of transparent, water, and ice materials. + +Validates each block name against MCC's `Material.cs` enum. Blocks without a matching enum value are skipped. + +## gen_entity_category_map.py -- Generate minimap entity category JSON + +Extracts entity-to-MobCategory mappings from decompiled Minecraft source for the TUI minimap. + +```bash +python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled +# -> MinecraftClient/Tui/MinimapEntityCategories.json +``` + +Parses `EntityType.java` to read each entity's `MobCategory` assignment from the `EntityType.Builder.of(Factory, MobCategory.XXX)` call. Maps Minecraft categories to MCC minimap categories: +- `MONSTER` -> hostile +- `CREATURE`/`AMBIENT`/`AXOLOTLS`/`WATER_*` -> passive +- `MISC` -> non_living + +The script maintains manual override lists for: +- **Neutral mobs** (e.g. Enderman, Spider, Wolf, Bee) -- Minecraft has no "neutral" category; these are MONSTER or CREATURE in code but only attack when provoked +- **Passive overrides** (e.g. Villager, WanderingTrader) -- classified as MISC in Minecraft for spawning reasons but should appear as passive on the minimap + +Output: `MinecraftClient/Tui/MinimapEntityCategories.json` (embedded as a resource via `.csproj`). Validates each entity name against MCC's `EntityType.cs` enum. + ## Recommended workflow 1. Generate server reports (Step 0) @@ -145,6 +183,9 @@ Uses `curl` with resume (`-C -`) for reliable download over slow connections. Fa - Entities: `gen_entity_palette.py` - Metadata: `gen_entity_metadata_palette.py` 4. Update block collision shapes: `gen_block_shapes.py` -5. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs` -6. Update version routing (see SKILL.md) -7. Build and test +5. Update minimap data (if blocks or entities changed): + - Block colors: `gen_block_color_map.py` + - Entity categories: `gen_entity_category_map.py` +6. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs` +7. Update version routing (see SKILL.md) +8. Build and test diff --git a/tools/decompile.sh b/tools/decompile.sh index cd17ab7a..791431ba 100644 --- a/tools/decompile.sh +++ b/tools/decompile.sh @@ -86,18 +86,90 @@ fi mkdir -p "$MC_OFFICIAL/remapped_jar" +# --- Resolve version metadata from Mojang manifest --- +MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json" +VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c " +import json, sys +data = json.load(sys.stdin) +for v in data['versions']: + if v['id'] == '$VERSION': + print(v['url']) + break +") +if [[ -z "$VERSION_URL" ]]; then + echo "Error: version $VERSION not found in Mojang launcher manifest." + exit 1 +fi + +VERSION_META=$(curl -sL "$VERSION_URL") +MAPPING_KEY="${SIDE_LOWER}_mappings" +HAS_MAPPINGS=$(echo "$VERSION_META" | python3 -c " +import json, sys +data = json.load(sys.stdin) +print('true' if '$MAPPING_KEY' in data.get('downloads', {}) else 'false') +") + echo "=== Decompiling Minecraft $VERSION ($SIDE) ===" echo " Remapped JAR: $REMAPPED_JAR" echo " Decompiled: $DECOMPILED_DIR" +echo " Obfuscated: $HAS_MAPPINGS" echo "" cd "$MC_OFFICIAL" -java -jar "$DECOMPILER_JAR" \ - --version "$VERSION" \ - --side "$SIDE" \ - --decompile \ - --output "$REMAPPED_JAR" \ - --decompiled-output "$DECOMPILED_DIR" + +if [[ "$HAS_MAPPINGS" == "true" ]]; then + # Obfuscated version: use --version/--side to auto-download jar + mappings + deobfuscate + java -jar "$DECOMPILER_JAR" \ + --version "$VERSION" \ + --side "$SIDE" \ + --decompile \ + --output "$REMAPPED_JAR" \ + --decompiled-output "$DECOMPILED_DIR" +else + # Unobfuscated version (26.1+): download jar, extract inner jar from bundle, decompile directly. + # MinecraftDecompiler requires --mapping-path with --input, but unobfuscated versions + # have no mappings. We use Vineflower directly instead. + echo "No Proguard mappings for $VERSION; decompiling without deobfuscation." + + JAR_URL=$(echo "$VERSION_META" | python3 -c " +import json, sys +data = json.load(sys.stdin) +print(data['downloads']['${SIDE_LOWER}']['url']) +") + ORIGINAL_JAR="$MC_OFFICIAL/remapped_jar/${VERSION}-${SIDE_LOWER}-original.jar" + if [[ ! -f "$ORIGINAL_JAR" ]]; then + echo "Downloading ${SIDE_LOWER}.jar ..." + curl -L -o "$ORIGINAL_JAR" "$JAR_URL" + fi + + # Since 1.18, server.jar is a bundled jar containing the actual game jar inside + # META-INF/versions//server-.jar. Extract it if present. + DECOMPILE_TARGET="$ORIGINAL_JAR" + EXTRACT_DIR=$(mktemp -d) + trap "rm -rf '$EXTRACT_DIR'" EXIT + if unzip -q -o "$ORIGINAL_JAR" "META-INF/versions.list" -d "$EXTRACT_DIR" 2>/dev/null; then + INNER_PATH=$(awk '{print $NF}' "$EXTRACT_DIR/META-INF/versions.list" | head -1) + if [[ -n "$INNER_PATH" ]]; then + unzip -q -o "$ORIGINAL_JAR" "META-INF/versions/$INNER_PATH" -d "$EXTRACT_DIR" + DECOMPILE_TARGET="$EXTRACT_DIR/META-INF/versions/$INNER_PATH" + echo "Extracted inner jar: $INNER_PATH" + fi + fi + + # Use Vineflower directly (bundled with MinecraftDecompiler, or standalone) + VINEFLOWER_JAR="$MC_OFFICIAL/downloads/decompiler/vineflower.jar" + if [[ ! -f "$VINEFLOWER_JAR" ]]; then + # Fall back to vineflower bundled inside MinecraftDecompiler's cache + VINEFLOWER_JAR=$(find "$MC_OFFICIAL" -name "vineflower*.jar" -not -name "MinecraftDecompiler.jar" 2>/dev/null | head -1) + fi + if [[ -z "$VINEFLOWER_JAR" || ! -f "$VINEFLOWER_JAR" ]]; then + echo "Error: vineflower.jar not found. Place it at $MC_OFFICIAL/downloads/decompiler/vineflower.jar" + exit 1 + fi + + echo "Decompiling with Vineflower: $VINEFLOWER_JAR" + java -jar "$VINEFLOWER_JAR" "$DECOMPILE_TARGET" "$DECOMPILED_DIR" +fi echo "" echo "=== Done ===" @@ -108,29 +180,18 @@ if [[ "$SIDE" == "SERVER" ]]; then DOWNLOADS_DIR="$MC_OFFICIAL/downloads/$VERSION" if [[ ! -f "$DOWNLOADS_DIR/server.jar" ]]; then mkdir -p "$DOWNLOADS_DIR" - # MinecraftDecompiler downloads the original jar into its cache; - # extract it from the bundled remapped jar or re-download via manifest. echo "" echo "Downloading server.jar for $VERSION into $DOWNLOADS_DIR ..." - MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json" - VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c " -import json, sys -data = json.load(sys.stdin) -for v in data['versions']: - if v['id'] == '$VERSION': - print(v['url']) - break -") - if [[ -n "$VERSION_URL" ]]; then - SERVER_JAR_URL=$(curl -sL "$VERSION_URL" | python3 -c " + SERVER_JAR_URL=$(echo "$VERSION_META" | python3 -c " import json, sys data = json.load(sys.stdin) print(data['downloads']['server']['url']) ") + if [[ -n "$SERVER_JAR_URL" ]]; then curl -L -o "$DOWNLOADS_DIR/server.jar" "$SERVER_JAR_URL" echo "Downloaded server.jar" else - echo "Warning: could not find version $VERSION in Mojang manifest; server.jar not downloaded." + echo "Warning: could not download server.jar for $VERSION." fi else echo "server.jar already exists: $DOWNLOADS_DIR/server.jar" diff --git a/tools/gen_block_color_map.py b/tools/gen_block_color_map.py new file mode 100644 index 00000000..69cbb502 --- /dev/null +++ b/tools/gen_block_color_map.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +""" +Generate MinimapBlockColors.json from decompiled Minecraft source. + +Parses MapColor.java for the 62 base map colors (ID -> RGB), then parses +Blocks.java to extract each block's mapColor assignment, and outputs a +JSON mapping from MCC Material enum names (PascalCase) to RGB triples. + +Usage: + python3 tools/gen_block_color_map.py + +Example: + python3 tools/gen_block_color_map.py MinecraftOfficial/26.1-rc-2-decompiled +""" + +import json +import re +import sys +from pathlib import Path + +OUTPUT_PATH = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Tui" / "MinimapBlockColors.json") +MATERIAL_CS = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Mapping" / "Material.cs") + + +def mc_name_to_csharp(mc_name: str) -> str: + name = mc_name.removeprefix("minecraft:") + return "".join(word.capitalize() for word in name.split("_")) + + +def parse_map_colors(map_color_java: Path) -> dict[str, tuple[int, int, int]]: + """Parse MapColor.java: extract name -> (R, G, B) for each constant.""" + text = map_color_java.read_text() + colors: dict[str, tuple[int, int, int]] = {} + + pattern = re.compile( + r'public static final MapColor\s+(\w+)\s*=\s*new\s+MapColor\(\s*(\d+)\s*,\s*(\d+)\s*\)') + for m in pattern.finditer(text): + name = m.group(1) + color_int = int(m.group(3)) + r = (color_int >> 16) & 0xFF + g = (color_int >> 8) & 0xFF + b = color_int & 0xFF + colors[name] = (r, g, b) + + return colors + + +def parse_dye_to_map_color(dye_color_java: Path) -> dict[str, str]: + """Parse DyeColor.java: extract DyeColor name -> MapColor name.""" + text = dye_color_java.read_text() + mapping: dict[str, str] = {} + + pattern = re.compile( + r'(\w+)\(\d+,\s*"[^"]+",\s*\d+,\s*MapColor\.(\w+)') + for m in pattern.finditer(text): + mapping[m.group(1)] = m.group(2) + + return mapping + + +def extract_block_declarations(text: str) -> list[tuple[str, str, str]]: + """Extract (field_name, block_id, full_register_body) for each block declaration. + + Returns list of (FIELD_NAME, "block_name", "register(...) content"). + """ + results = [] + + # Find all "public static final Block FIELD = register(...)" declarations. + # These span multiple lines and end with ");". + # Strategy: find start pattern, then track parens to find matching end. + field_pattern = re.compile( + r'public\s+static\s+final\s+Block\s+(\w+)\s*=\s*register\s*\(') + + pos = 0 + while pos < len(text): + m = field_pattern.search(text, pos) + if not m: + break + + field_name = m.group(1) + paren_start = m.end() - 1 # position of opening '(' + + # Find matching closing ')' then ';' + depth = 1 + i = paren_start + 1 + while i < len(text) and depth > 0: + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + i += 1 + + register_body = text[paren_start:i] + + # Extract block name string from register call + name_match = re.search(r'(?:BlockIds\.(\w+)|"(\w+)")', register_body) + if name_match: + raw_id = name_match.group(1) or name_match.group(2) + block_id = raw_id.lower() if raw_id.isupper() else raw_id + else: + block_id = field_name.lower() + + results.append((field_name, block_id, register_body)) + pos = i + + return results + + +def parse_blocks(blocks_java: Path, map_colors: dict[str, tuple[int, int, int]], + dye_to_map: dict[str, str]) -> dict[str, tuple[int, int, int]]: + """Parse Blocks.java: extract block_name -> (R, G, B).""" + text = blocks_java.read_text() + + declarations = extract_block_declarations(text) + print(f" Found {len(declarations)} block register() declarations") + + # First pass: assign MapColor name to each block + field_to_block_id: dict[str, str] = {} + block_color_name: dict[str, str] = {} + + map_color_direct = re.compile(r'\.mapColor\(MapColor\.(\w+)\)') + map_color_dye = re.compile(r'\.mapColor\(DyeColor\.(\w+)\)') + map_color_ref = re.compile(r'\.mapColor\((\w+)\.defaultMapColor\(\)') + map_color_waterlogged = re.compile(r'\.mapColor\(waterloggedMapColor\(MapColor\.(\w+)\)') + + for field_name, block_id, body in declarations: + field_to_block_id[field_name] = block_id + + mc = map_color_direct.search(body) + if mc: + block_color_name[block_id] = mc.group(1) + continue + + mc = map_color_dye.search(body) + if mc: + dye_name = mc.group(1) + if dye_name in dye_to_map: + block_color_name[block_id] = dye_to_map[dye_name] + continue + + mc = map_color_waterlogged.search(body) + if mc: + block_color_name[block_id] = mc.group(1) + continue + + mc = map_color_ref.search(body) + if mc: + ref_field = mc.group(1) + ref_block = field_to_block_id.get(ref_field) + if ref_block and ref_block in block_color_name: + block_color_name[block_id] = block_color_name[ref_block] + + # Second pass: resolve remaining BLOCK.defaultMapColor() references + for field_name, block_id, body in declarations: + if block_id in block_color_name: + continue + mc = map_color_ref.search(body) + if mc: + ref_field = mc.group(1) + ref_block = field_to_block_id.get(ref_field) + if ref_block and ref_block in block_color_name: + block_color_name[block_id] = block_color_name[ref_block] + + result: dict[str, tuple[int, int, int]] = {} + for block_id, color_name in block_color_name.items(): + if color_name in map_colors: + cs_name = mc_name_to_csharp(block_id) + result[cs_name] = map_colors[color_name] + + return result + + +def load_known_materials() -> set[str]: + known = set() + if MATERIAL_CS.exists(): + with open(MATERIAL_CS) as f: + for line in f: + m = re.match(r'\s+(\w+),?\s*$', line) + if m: + known.add(m.group(1)) + return known + + +TRANSPARENT_BLOCKS = [ + "Air", "CaveAir", "VoidAir", + "Glass", "GlassPane", + "WhiteStainedGlass", "OrangeStainedGlass", "MagentaStainedGlass", + "LightBlueStainedGlass", "YellowStainedGlass", "LimeStainedGlass", + "PinkStainedGlass", "GrayStainedGlass", "LightGrayStainedGlass", + "CyanStainedGlass", "PurpleStainedGlass", "BlueStainedGlass", + "BrownStainedGlass", "GreenStainedGlass", "RedStainedGlass", + "BlackStainedGlass", + "WhiteStainedGlassPane", "OrangeStainedGlassPane", "MagentaStainedGlassPane", + "LightBlueStainedGlassPane", "YellowStainedGlassPane", "LimeStainedGlassPane", + "PinkStainedGlassPane", "GrayStainedGlassPane", "LightGrayStainedGlassPane", + "CyanStainedGlassPane", "PurpleStainedGlassPane", "BlueStainedGlassPane", + "BrownStainedGlassPane", "GreenStainedGlassPane", "RedStainedGlassPane", + "BlackStainedGlassPane", + "TintedGlass", "Barrier", "Light", "StructureVoid", +] + +WATER_BLOCKS = ["Water"] +ICE_BLOCKS = ["Ice", "PackedIce", "BlueIce", "FrostedIce"] + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + root = Path(sys.argv[1]) + if not root.is_dir(): + print(f"Error: {root} is not a directory") + sys.exit(1) + + map_color_java = root / "net/minecraft/world/level/material/MapColor.java" + dye_color_java = root / "net/minecraft/world/item/DyeColor.java" + blocks_java = root / "net/minecraft/world/level/block/Blocks.java" + + for f in [map_color_java, dye_color_java, blocks_java]: + if not f.exists(): + print(f"Error: {f} not found") + sys.exit(1) + + print("Parsing MapColor.java...") + map_colors = parse_map_colors(map_color_java) + print(f" Found {len(map_colors)} map colors") + + print("Parsing DyeColor.java...") + dye_to_map = parse_dye_to_map_color(dye_color_java) + print(f" Found {len(dye_to_map)} dye->map color mappings") + + print("Parsing Blocks.java...") + block_colors = parse_blocks(blocks_java, map_colors, dye_to_map) + print(f" Extracted colors for {len(block_colors)} blocks") + + known_materials = load_known_materials() + if known_materials: + matched = {k: v for k, v in block_colors.items() if k in known_materials} + unmatched = [k for k in block_colors if k not in known_materials] + if unmatched: + print(f"\n {len(unmatched)} blocks not in Material.cs (will be skipped):") + for name in sorted(unmatched)[:20]: + print(f" {name}") + if len(unmatched) > 20: + print(f" ... and {len(unmatched) - 20} more") + block_colors = matched + print(f" {len(block_colors)} blocks matched to Material.cs entries") + + output = { + "version": root.name.replace("-decompiled", "").replace("-client", ""), + "colors": {k: list(v) for k, v in sorted(block_colors.items())}, + "transparent": sorted(TRANSPARENT_BLOCKS), + "water": WATER_BLOCKS, + "ice": ICE_BLOCKS, + } + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, 'w') as f: + json.dump(output, f, indent=2) + print(f"\nGenerated {OUTPUT_PATH}") + print(f" {len(block_colors)} color entries") + + +if __name__ == "__main__": + main() diff --git a/tools/gen_entity_category_map.py b/tools/gen_entity_category_map.py new file mode 100644 index 00000000..e258d186 --- /dev/null +++ b/tools/gen_entity_category_map.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +Generate MinimapEntityCategories.json from decompiled Minecraft source. + +Parses EntityType.java to extract each entity's MobCategory assignment, +then maps them to MCC minimap categories (hostile/passive/neutral/non_living). + +Minecraft's MobCategory values: + MONSTER -> hostile (with neutral overrides for conditionally hostile mobs) + CREATURE -> passive (with neutral overrides for conditionally hostile mobs) + AMBIENT -> passive + AXOLOTLS -> passive + WATER_CREATURE -> passive + WATER_AMBIENT -> passive + UNDERGROUND_WATER_CREATURE -> passive + MISC -> non_living + +Some mobs classified as MONSTER or CREATURE are actually "neutral" -- they +only attack when provoked. These are listed in NEUTRAL_OVERRIDES below and +should be updated when new conditionally-hostile mobs are added. + +Usage: + python3 tools/gen_entity_category_map.py + +Example: + python3 tools/gen_entity_category_map.py MinecraftOfficial/26.1-rc-2-decompiled +""" + +import json +import re +import sys +from pathlib import Path + +OUTPUT_PATH = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Tui" / "MinimapEntityCategories.json") +ENTITY_TYPE_CS = (Path(__file__).resolve().parent.parent + / "MinecraftClient" / "Mapping" / "EntityType.cs") + + +def mc_name_to_csharp(mc_name: str) -> str: + name = mc_name.removeprefix("minecraft:") + return "".join(word.capitalize() for word in name.split("_")) + + +# Mobs that Minecraft classifies as MONSTER or CREATURE but behave as +# "neutral" -- they only attack when provoked. This list is maintained +# manually because there is no machine-readable flag in the game data. +NEUTRAL_OVERRIDES = { + "bee", "dolphin", "goat", "iron_golem", "llama", "panda", + "polar_bear", "snow_golem", "trader_llama", "wolf", + "zombified_piglin", "enderman", "spider", "cave_spider", + "copper_golem", +} + +# Entities whose MobCategory in the game code doesn't match how they +# should appear on the minimap. For example, Villager and WanderingTrader +# are MISC in MC code (for spawning reasons) but should be passive on the map. +# ZombieHorse is MONSTER but is a rideable passive mob in practice. +PASSIVE_OVERRIDES = { + "villager", "wandering_trader", "zombie_horse", +} + +# Player has its own category in MCC -- extracted from MISC to "player". +PLAYER_OVERRIDES = {"player"} + +MC_TO_MCC = { + "MONSTER": "hostile", + "CREATURE": "passive", + "AMBIENT": "passive", + "AXOLOTLS": "passive", + "WATER_CREATURE": "passive", + "WATER_AMBIENT": "passive", + "UNDERGROUND_WATER_CREATURE": "passive", + "MISC": "non_living", +} + + +def extract_entity_categories(entity_type_java: Path) -> list[tuple[str, str, str]]: + """Extract (entity_id, field_name, MobCategory) from EntityType.java. + + Returns list of (entity_id, FIELD_NAME, MobCategory_name). + """ + text = entity_type_java.read_text() + results = [] + + field_pat = re.compile( + r'public\s+static\s+final\s+EntityType<[^>]+>\s+(\w+)\s*=\s*register\s*\(') + + pos = 0 + while pos < len(text): + m = field_pat.search(text, pos) + if not m: + break + + field_name = m.group(1) + paren_start = m.end() - 1 + depth = 1 + i = paren_start + 1 + while i < len(text) and depth > 0: + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + i += 1 + + body = text[paren_start:i] + + name_match = re.search(r'"(\w+)"', body) + entity_id = name_match.group(1) if name_match else field_name.lower() + + cat_match = re.search(r'MobCategory\.(\w+)', body) + mob_cat = cat_match.group(1) if cat_match else "MISC" + + results.append((entity_id, field_name, mob_cat)) + pos = i + + return results + + +def load_known_entity_types() -> set[str]: + known = set() + if ENTITY_TYPE_CS.exists(): + with open(ENTITY_TYPE_CS) as f: + for line in f: + m = re.match(r'\s+(\w+),?\s*$', line) + if m: + known.add(m.group(1)) + return known + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + sys.exit(1) + + root = Path(sys.argv[1]) + entity_type_java = root / "net/minecraft/world/entity/EntityType.java" + + if not entity_type_java.exists(): + print(f"Error: {entity_type_java} not found") + sys.exit(1) + + print("Parsing EntityType.java...") + entities = extract_entity_categories(entity_type_java) + print(f" Found {len(entities)} entity type declarations") + + known_types = load_known_entity_types() + + hostile = [] + passive = [] + neutral = [] + non_living = [] + + for entity_id, field_name, mob_cat in entities: + cs_name = mc_name_to_csharp(entity_id) + + if known_types and cs_name not in known_types: + continue + + if entity_id in PLAYER_OVERRIDES: + continue + elif entity_id in NEUTRAL_OVERRIDES: + neutral.append(cs_name) + elif entity_id in PASSIVE_OVERRIDES: + passive.append(cs_name) + elif mob_cat in MC_TO_MCC: + cat = MC_TO_MCC[mob_cat] + if cat == "hostile": + hostile.append(cs_name) + elif cat == "passive": + passive.append(cs_name) + elif cat == "non_living": + non_living.append(cs_name) + else: + non_living.append(cs_name) + else: + non_living.append(cs_name) + + output = { + "version": root.name.replace("-decompiled", "").replace("-client", ""), + "hostile": sorted(hostile), + "passive": sorted(passive), + "neutral": sorted(neutral), + "non_living": sorted(non_living), + } + + OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_PATH, 'w') as f: + json.dump(output, f, indent=2) + + print(f"\nGenerated {OUTPUT_PATH}") + print(f" hostile: {len(hostile)}") + print(f" passive: {len(passive)}") + print(f" neutral: {len(neutral)}") + print(f" non_living: {len(non_living)}") + print(f" total: {len(hostile) + len(passive) + len(neutral) + len(non_living)}") + + +if __name__ == "__main__": + main() diff --git a/tools/mcc-debug.sh b/tools/mcc-debug.sh index b29f4b20..c5c6205e 100644 --- a/tools/mcc-debug.sh +++ b/tools/mcc-debug.sh @@ -32,6 +32,7 @@ EOF VERSION="1.21.11-Vanilla" MODE="classic" PORT="25565" +PORT_SET_BY_USER=false DO_BUILD=true DEBUG_ON=false FILE_INPUT=false @@ -40,7 +41,7 @@ while [[ $# -gt 0 ]]; do case "$1" in -v|--version) VERSION="$2"; shift 2 ;; -m|--mode) MODE="$2"; shift 2 ;; - -p|--port) PORT="$2"; shift 2 ;; + -p|--port) PORT="$2"; PORT_SET_BY_USER=true; shift 2 ;; --no-build) DO_BUILD=false; shift ;; --debug-on) DEBUG_ON=true; shift ;; --file-input) FILE_INPUT=true; shift ;; @@ -54,6 +55,10 @@ CFG="$TEST_ROOT/MinecraftClient.debug.ini" MCC_LOG="$TEST_ROOT/mcc-debug.log" INPUT_FILE="$REPO_ROOT/mcc_input.txt" SESSION_NAME="mc-${VERSION//\./_}" +PREPARE_CFG_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" +ENSURE_SERVER_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" +PREFLIGHT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" +GET_PORT_SCRIPT="$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" mkdir -p "$TEST_ROOT" @@ -64,6 +69,8 @@ echo " Config: $CFG" echo " Log: $MCC_LOG" echo "" +bash "$PREFLIGHT_SCRIPT" "$VERSION" >/dev/null + # --- Build --- if $DO_BUILD; then echo "[1/4] Building MCC..." @@ -75,21 +82,22 @@ fi # --- Prepare config --- echo "[2/4] Preparing config..." -cp "$REPO_ROOT/MinecraftClient.ini" "$CFG" - -sed -i \ - -e 's/Account = { Login = "[^"]*", Password = "[^"]*" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ - -e 's/InventoryHandling = false/InventoryHandling = true/' \ - -e 's/EntityHandling = false/EntityHandling = true/' \ - "$CFG" +bash "$PREPARE_CFG_SCRIPT" "$CFG" "${VERSION%-Vanilla}" CursorBot >/dev/null if [[ "$MODE" == "tui" ]]; then - sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" + else + sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG" + fi fi if $DEBUG_ON; then - sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG" + if [[ "$(uname)" == "Darwin" ]]; then + sed -i '' 's/DebugMessages = false/DebugMessages = true/' "$CFG" + else + sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG" + fi fi echo " Config ready" @@ -99,14 +107,7 @@ echo "[3/4] Starting server $VERSION..." if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then echo " Server already running" else - # Ensure offline mode - SERVER_DIR="$MCC_SERVERS/$VERSION" - if [[ -f "$SERVER_DIR/server.properties" ]]; then - sed -i 's/^online-mode=.*/online-mode=false/' "$SERVER_DIR/server.properties" - grep -q "^enable-rcon=" "$SERVER_DIR/server.properties" || echo "enable-rcon=true" >> "$SERVER_DIR/server.properties" - grep -q "^rcon.password=" "$SERVER_DIR/server.properties" || echo "rcon.password=test123" >> "$SERVER_DIR/server.properties" - grep -q "^rcon.port=" "$SERVER_DIR/server.properties" || echo "rcon.port=25575" >> "$SERVER_DIR/server.properties" - fi + bash "$ENSURE_SERVER_SCRIPT" "$VERSION" >/dev/null mc-start "$VERSION" >/dev/null echo -n " Waiting for server..." @@ -125,6 +126,10 @@ else done fi +if ! $PORT_SET_BY_USER; then + PORT="$(bash "$GET_PORT_SCRIPT" "$VERSION")" +fi + # --- Launch MCC --- echo "[4/4] Launching MCC in $MODE mode..." : > "$INPUT_FILE" diff --git a/tools/mcc-env.sh b/tools/mcc-env.sh index 6ddca998..904a8a00 100644 --- a/tools/mcc-env.sh +++ b/tools/mcc-env.sh @@ -26,6 +26,9 @@ mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; } mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; } mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; } mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; } +mc-wait-ready() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "${1:-1.20.6}" >/dev/null && source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_ready "${1:-1.20.6}" "${2:-60}"; } +mc-wait-stop() { source "$MCC_REPO/.skills/mcc-integration-testing/scripts/common.sh" && wait_for_server_stop "${1:-1.20.6}" "${2:-60}"; } +mc-reset-test-env() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" "$@"; } # --- RCON --- mc-rcon() { bash "$MCC_REPO/tools/mc-rcon.sh" "$@"; } @@ -59,3 +62,4 @@ mcc-tui() { mcc-debug() { bash "$MCC_REPO/tools/mcc-debug.sh" "$@"; } mcc-log-mcc() { tail -f "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null || echo "No MCC log found"; } mcc-state() { echo "debug state" >> "$MCC_REPO/mcc_input.txt"; sleep 1; tail -30 "${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log" 2>/dev/null; } +mcc-preflight() { bash "$MCC_REPO/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$@"; } diff --git a/tools/run-creative-e2e.sh b/tools/run-creative-e2e.sh index 35555ad9..dce850d4 100644 --- a/tools/run-creative-e2e.sh +++ b/tools/run-creative-e2e.sh @@ -5,6 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # shellcheck source=tools/mcc-env.sh source "$REPO_ROOT/tools/mcc-env.sh" +# shellcheck source=.skills/mcc-integration-testing/scripts/common.sh +source "$REPO_ROOT/.skills/mcc-integration-testing/scripts/common.sh" usage() { cat <<'EOF' @@ -19,6 +21,7 @@ EOF SERVER_DIR="${1:-}" MC_VERSION="${2:-}" PROFILE="${3:-}" +SERVER_PORT="" if [[ -z "$SERVER_DIR" || -z "$MC_VERSION" || -z "$PROFILE" ]]; then usage >&2 @@ -37,6 +40,7 @@ MCC_LOG="$TEST_ROOT/mcc.log" SERVER_LOG_FILE="$MCC_SERVERS/$SERVER_DIR/logs/latest.log" INPUT_FILE="$REPO_ROOT/mcc_input.txt" MCC_PID="" +SERVER_PORT="25565" mkdir -p "$TEST_ROOT" @@ -59,33 +63,21 @@ wait_for_file_pattern() { return 1 } -wait_for_server_ready() { - local timeout="${1:-60}" +wait_for_rcon_port_free() { + local timeout="${1:-30}" local elapsed=0 while (( elapsed < timeout )); do - if mc-log "$SERVER_DIR" 250 2>/dev/null | grep -Fq "Done ("; then + if ! ss -ltn '( sport = :25575 )' 2>/dev/null | grep -Fq ':25575'; then return 0 fi sleep 1 ((elapsed += 1)) done - echo "Timed out waiting for server readiness" >&2 + echo "Timed out waiting for RCON port 25575 to become free" >&2 return 1 } - -kill_other_servers() { - local sessions - sessions="$(tmux list-sessions 2>/dev/null | awk -F: '/^mc-/{print $1}' || true)" - if [[ -n "$sessions" ]]; then - while IFS= read -r session; do - [[ -z "$session" ]] && continue - tmux kill-session -t "$session" 2>/dev/null || true - done <<< "$sessions" - fi -} - cleanup() { if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then echo "quit" >> "$INPUT_FILE" 2>/dev/null || true @@ -96,33 +88,28 @@ cleanup() { if [[ -p "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" ]]; then echo "stop" > "$MCC_SERVERS/$SERVER_DIR/stdin.pipe" 2>/dev/null || true - sleep 2 + wait_for_server_stop "$SERVER_DIR" 20 >/dev/null 2>&1 || true fi tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true + wait_for_rcon_port_free 30 || true } trap cleanup EXIT prepare_config() { - cp "$REPO_ROOT/MinecraftClient.ini" "$CFG" + MCC_TEST_ACCOUNT_TYPE=mojang MCC_TEST_PASSWORD=- \ + bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" \ + "$REPO_ROOT/MinecraftClient.ini" "$CFG" "$MC_VERSION" CursorBot >/dev/null - sed -i \ - -e 's/Account = { Login = "test", Password = "-" }/Account = { Login = "CursorBot", Password = "-" }/' \ - -e "s/MinecraftVersion = \"auto\"/MinecraftVersion = \"$MC_VERSION\"/" \ + sed_in_place \ + -e "s#^Server = .*#Server = { Host = \"localhost\", Port = $SERVER_PORT }#" \ -e 's/TerrainAndMovements = false/TerrainAndMovements = true/' \ -e 's/InventoryHandling = false/InventoryHandling = true/' \ -e 's/EntityHandling = false/EntityHandling = true/' \ -e 's/AutoRespawn = false/AutoRespawn = true/' \ "$CFG" - - sed -i '/^\[ChatBot.ScriptScheduler\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.DiscordRpc\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.AntiAFK\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.AutoDig\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.AutoAttack\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.PlayerListLogger\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" - sed -i '/^\[ChatBot.ReplayCapture\]/,/^\[/ { s/^Enabled = true/Enabled = false/; }' "$CFG" + disable_noisy_bots_in_ini "$CFG" } send_mcc_command() { @@ -195,23 +182,31 @@ modern_mob_and_effects() { run_server_command "effect give CursorBot minecraft:regeneration 10 1 true" } -prepare_config -kill_other_servers +bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/preflight_test_env.sh" "$SERVER_DIR" >/dev/null +bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh" --all >/dev/null +wait_for_rcon_port_free 30 || true rm -f "$MCC_LOG" "$INPUT_FILE" bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh" "$SERVER_DIR" >/dev/null +SERVER_PORT="$(bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/get_server_port.sh" "$SERVER_DIR")" if [[ -f "$MCC_SERVERS/$SERVER_DIR/server.properties" ]]; then - sed -i 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" + sed_in_place 's/^use-native-transport=.*/use-native-transport=false/' "$MCC_SERVERS/$SERVER_DIR/server.properties" fi mc-start "$SERVER_DIR" >/dev/null -wait_for_server_ready || exit 1 +wait_for_server_ready "$SERVER_DIR" || exit 1 +prepare_config : > "$INPUT_FILE" ( cd "$REPO_ROOT" - MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" > "$MCC_LOG" 2>&1 + MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- \ + "$CFG" \ + CursorBot \ + - \ + "localhost:$SERVER_PORT" \ + > "$MCC_LOG" 2>&1 ) & MCC_PID=$! diff --git a/tools/start-server.sh b/tools/start-server.sh index 9debbae9..63e5eed3 100644 --- a/tools/start-server.sh +++ b/tools/start-server.sh @@ -1,12 +1,38 @@ #!/bin/bash # Start a Minecraft server in a tmux session with named pipe for stdin # Servers live under $MCC_SERVERS or default to MinecraftOfficial/downloads//. +resolve_java_bin() { + if command -v java >/dev/null 2>&1 && java -version >/dev/null 2>&1; then + command -v java + return 0 + fi + + local candidate + for candidate in \ + "${JAVA_BIN:-}" \ + "/opt/homebrew/opt/openjdk/bin/java" \ + "/usr/local/opt/openjdk/bin/java" \ + "/usr/lib/jvm/default-java/bin/java" + do + [[ -z "$candidate" ]] && continue + if [[ -x "$candidate" ]]; then + if "$candidate" -version >/dev/null 2>&1; then + printf '%s\n' "$candidate" + return 0 + fi + fi + done + + return 1 +} + VERSION="${1}" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" DOWNLOADS="${MCC_SERVERS:-$REPO_ROOT/MinecraftOfficial/downloads}" DIR="$DOWNLOADS/$VERSION" PIPE="$DIR/stdin.pipe" SESSION="mc-${VERSION//\./_}" +JAVA_BIN="$(resolve_java_bin || true)" if [ -z "$VERSION" ] || [ ! -d "$DIR" ]; then echo "Error: Server directory not found${VERSION:+: $DIR}" @@ -20,6 +46,16 @@ if [ ! -f "$DIR/server.jar" ]; then exit 1 fi +if ! command -v tmux >/dev/null 2>&1; then + echo "Error: tmux is required to start local test servers" + exit 1 +fi + +if [[ -z "$JAVA_BIN" ]]; then + echo "Error: Java was not found on PATH. Install Java or set JAVA_BIN." >&2 + exit 1 +fi + if tmux has-session -t "$SESSION" 2>/dev/null; then echo "Server $VERSION already running in tmux session '$SESSION'" echo "View output: tmux capture-pane -t '$SESSION' -p -S -50" @@ -29,10 +65,14 @@ fi rm -f "$DIR/world/session.lock" +if [[ -e "$PIPE" && ! -p "$PIPE" ]]; then + rm -f "$PIPE" +fi + [ -p "$PIPE" ] || mkfifo "$PIPE" tmux new-session -d -s "$SESSION" -c "$DIR" \ - "tail -f $PIPE | java -Xmx2G -Xms2G -jar server.jar nogui 2>&1" + "tail -f $PIPE | '$JAVA_BIN' -Xmx2G -Xms2G -jar server.jar nogui 2>&1" echo "Server $VERSION started in tmux session '$SESSION'" echo "Send commands: echo 'say hello' > $PIPE"