mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge origin/master into feat/mcp-server
This commit is contained in:
commit
f4c160979c
86 changed files with 16765 additions and 2758 deletions
18
.github/workflows/build-and-release.yml
vendored
18
.github/workflows/build-and-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -436,3 +436,8 @@ FodyWeavers.xsd
|
|||
# SpecStory files
|
||||
/.specstory/
|
||||
/.vscode/settings.json
|
||||
|
||||
# Other
|
||||
/Sentry/
|
||||
/downloads/
|
||||
server.pid
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ metadata:
|
|||
- slow
|
||||
- hang
|
||||
- deadlock
|
||||
version: 0.2.0
|
||||
---
|
||||
|
||||
# C#/.NET CLI Optimization
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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:<server-port>` 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.
|
||||
|
|
|
|||
110
.skills/mcc-integration-testing/scripts/common.sh
Executable file
110
.skills/mcc-integration-testing/scripts/common.sh
Executable file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
48
.skills/mcc-integration-testing/scripts/preflight_test_env.sh
Executable file
48
.skills/mcc-integration-testing/scripts/preflight_test_env.sh
Executable file
|
|
@ -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)"
|
||||
|
|
@ -1,15 +1,40 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
||||
echo "Usage: $0 <template-ini> <output-ini> <mc-version> [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 <output-ini> <mc-version> [login]
|
||||
prepare_offline_mcc_config.sh <template-ini> <output-ini> <mc-version> [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
|
||||
|
|
|
|||
50
.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh
Executable file
50
.skills/mcc-integration-testing/scripts/reset_shared_test_state.sh
Executable file
|
|
@ -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 | <server-dir>...]
|
||||
|
||||
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"
|
||||
168
.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh
Executable file
168
.skills/mcc-integration-testing/scripts/run_achievements_matrix.sh
Executable file
|
|
@ -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"
|
||||
396
.skills/mcc-integration-testing/scripts/run_achievements_test.sh
Executable file
396
.skills/mcc-integration-testing/scripts/run_achievements_test.sh
Executable file
|
|
@ -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] <server-dir> <mc-version> <legacy|modern>
|
||||
|
||||
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" <<EOF
|
||||
//MCCScript 1.0
|
||||
|
||||
MCC.LoadBot(new AchievementProbeBot());
|
||||
|
||||
//MCCScript Extensions
|
||||
|
||||
public class AchievementProbeBot : ChatBot
|
||||
{
|
||||
private const string TargetId = "$TARGET_ID";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
LogToConsole("[ACH_TEST] probe initialized");
|
||||
DumpState("initialize");
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
LogToConsole("[ACH_TEST] after join");
|
||||
DumpState("after_join");
|
||||
}
|
||||
|
||||
public override void OnAchievementUpdate(IReadOnlyList<Achievement> updated, IReadOnlyList<string> 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."
|
||||
|
|
@ -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..."
|
||||
(
|
||||
|
|
|
|||
57
.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh
Executable file
57
.skills/mcc-integration-testing/scripts/summarize_achievements_matrix.sh
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: summarize_achievements_matrix.sh <matrix-run-dir>" >&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."
|
||||
|
|
@ -15,6 +15,7 @@ Systematic workflow for updating Minecraft Console Client to support a new Minec
|
|||
$MCC_REPO/tools/decompile.sh --version <ver>
|
||||
```
|
||||
This auto-downloads `MinecraftDecompiler.jar` if needed, produces the decompiled source, and downloads `server.jar` into `$MCC_SERVERS/<ver>/`.
|
||||
- `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/<ver>/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/<version>/` (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/<version>/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/<version>-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/<version>-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) |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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/<version>-decompiled/`
|
||||
|
||||
## Build / Run
|
||||
- Init submodules first: `git submodule update --init --recursive`
|
||||
|
|
|
|||
36
MinecraftClient/Achievement.cs
Normal file
36
MinecraftClient/Achievement.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of an achievement or advancement.
|
||||
/// </summary>
|
||||
public enum AchievementType
|
||||
{
|
||||
Task,
|
||||
Challenge,
|
||||
Goal,
|
||||
Legacy
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Minecraft achievement (pre-1.12) or advancement (1.12+).
|
||||
/// </summary>
|
||||
/// <param name="Id">Resource identifier, e.g. "minecraft:story/root" or "achievement.openInventory"</param>
|
||||
/// <param name="Title">Display title (null for legacy achievements without display info)</param>
|
||||
/// <param name="Description">Display description (null for legacy achievements without display info)</param>
|
||||
/// <param name="Type">The frame type / achievement category</param>
|
||||
/// <param name="IsHidden">Whether this advancement is hidden in the UI</param>
|
||||
/// <param name="IsCompleted">Whether all requirements have been met</param>
|
||||
/// <param name="Requirements">OR-groups of criterion names; all groups must be satisfied</param>
|
||||
/// <param name="CriteriaProgress">Per-criterion completion status</param>
|
||||
public record Achievement(
|
||||
string Id,
|
||||
string? Title,
|
||||
string? Description,
|
||||
AchievementType Type,
|
||||
bool IsHidden,
|
||||
bool IsCompleted,
|
||||
IReadOnlyList<IReadOnlyList<string>> Requirements,
|
||||
IReadOnlyDictionary<string, bool> CriteriaProgress);
|
||||
}
|
||||
|
|
@ -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<MaxDamageComponent>().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;
|
||||
|
|
|
|||
|
|
@ -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<ItemType, uint> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when detected a fish is caught
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<string> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
106
MinecraftClient/Commands/AchievementCommand.cs
Normal file
106
MinecraftClient/Commands/AchievementCommand.cs
Normal file
|
|
@ -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 <list|locked|unlocked>";
|
||||
public override string CmdDesc => Translations.cmd_achievement_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> 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
|
||||
});
|
||||
}
|
||||
|
||||
/// <param name="completed">null = all, true = unlocked only, false = locked only</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
284
MinecraftClient/Commands/Minimap.cs
Normal file
284
MinecraftClient/Commands/Minimap.cs
Normal file
|
|
@ -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<CmdResult> 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";
|
||||
}
|
||||
}
|
||||
98
MinecraftClient/Commands/RecipeBook.cs
Normal file
98
MinecraftClient/Commands/RecipeBook.cs
Normal file
|
|
@ -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 <list|craft|craftall> [recipe id]";
|
||||
public override string CmdDesc => Translations.cmd_recipebook_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
77
MinecraftClient/Commands/Teams.cs
Normal file
77
MinecraftClient/Commands/Teams.cs
Normal file
|
|
@ -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<CmdResult> 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<string, PlayerTeam> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
63
MinecraftClient/Commands/Tryout.cs
Normal file
63
MinecraftClient/Commands/Tryout.cs
Normal file
|
|
@ -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<CmdResult> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <x> <y> <z>"; } }
|
||||
public override string CmdUsage { get { return "useblock <x> <y> <z> [mainhand|offhand]"; } }
|
||||
public override string CmdDesc { get { return Translations.cmd_useblock_desc; } }
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<char> 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<char> 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<char> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<string>(out var s) => s,
|
||||
_ => node.ToJsonString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
MinecraftClient/LegacyAchievementCatalog.cs
Normal file
53
MinecraftClient/LegacyAchievementCatalog.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal static class LegacyAchievementCatalog
|
||||
{
|
||||
public static IReadOnlyList<string> 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<string> s_idSet = new(Ids, StringComparer.Ordinal);
|
||||
|
||||
public static bool Contains(string id)
|
||||
{
|
||||
return s_idSet.Contains(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
1326
MinecraftClient/Mapping/BlockHardness.cs
Normal file
1326
MinecraftClient/Mapping/BlockHardness.cs
Normal file
File diff suppressed because it is too large
Load diff
572
MinecraftClient/Mapping/MiningCalculator.cs
Normal file
572
MinecraftClient/Mapping/MiningCalculator.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class MiningCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute the number of ticks required to break a block in survival mode.
|
||||
/// Returns 0 for instant-break blocks, -1 for unbreakable blocks.
|
||||
/// </summary>
|
||||
/// <param name="blockMaterial">The block material to break</param>
|
||||
/// <param name="heldItem">The item in the player's main hand (null for empty hand)</param>
|
||||
/// <param name="helmetItem">The item in the player's helmet slot (null if empty, used for Aqua Affinity)</param>
|
||||
/// <param name="effects">Currently active player effects</param>
|
||||
/// <param name="playerAttributes">Cached player attribute values (from OnEntityProperties)</param>
|
||||
/// <param name="isUnderwater">Whether the player's eyes are submerged in water</param>
|
||||
/// <param name="isOnGround">Whether the player is on the ground</param>
|
||||
/// <param name="protocolVersion">The Minecraft protocol version</param>
|
||||
/// <returns>Ticks to break the block, 0 for instant, -1 for unbreakable</returns>
|
||||
public static int ComputeDigTicks(
|
||||
Material blockMaterial,
|
||||
Item? heldItem,
|
||||
Item? helmetItem,
|
||||
Dictionary<Effects, EffectData> effects,
|
||||
Dictionary<string, double> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the player's destroy speed for a given block, following vanilla formulas.
|
||||
/// </summary>
|
||||
private static float GetDestroySpeed(
|
||||
Material blockMaterial,
|
||||
Item? heldItem,
|
||||
Item? helmetItem,
|
||||
Dictionary<Effects, EffectData> effects,
|
||||
Dictionary<string, double> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<RuleSubComponent>? 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the tool provides correct drops for a block.
|
||||
/// </summary>
|
||||
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<RuleSubComponent>? 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<RuleSubComponent>? rules,
|
||||
out float defaultMiningSpeed)
|
||||
{
|
||||
rules = null;
|
||||
defaultMiningSpeed = 1.0f;
|
||||
|
||||
if (heldItem.Components is null)
|
||||
return false;
|
||||
|
||||
if (heldItem.Components.OfType<ToolComponent>().FirstOrDefault() is ToolComponent toolComponent)
|
||||
{
|
||||
rules = toolComponent.Rules;
|
||||
defaultMiningSpeed = toolComponent.DefaultMiningSpeed;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (heldItem.Components.OfType<ToolComponent1215>().FirstOrDefault() is ToolComponent1215 toolComponent1215)
|
||||
{
|
||||
rules = toolComponent1215.Rules;
|
||||
defaultMiningSpeed = toolComponent1215.DefaultMiningSpeed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a block material against a ToolComponent BlockSetSubcomponent.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approximate block tag matching using Material2Tool categories.
|
||||
/// Tags like "minecraft:mineable/pickaxe" map to the appropriate tool categories.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the enchantment level from an item, supporting both legacy NBT and modern structured components.
|
||||
/// </summary>
|
||||
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<string, object> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map Enchantments enum to Minecraft resource name (e.g., "efficiency").
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Legacy tool speed for pre-1.20.6 versions using hardcoded values.
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the held tool is the correct tool for drops in legacy versions.
|
||||
/// Uses Material2Tool's recommendations to determine correctness.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the minimum tool tier required for a block based on Material2Tool's recommendation ordering.
|
||||
/// </summary>
|
||||
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<string, double> 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, EffectData> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
49
MinecraftClient/Mapping/PlayerTeam.cs
Normal file
49
MinecraftClient/Mapping/PlayerTeam.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Minecraft scoreboard team and its current state.
|
||||
/// </summary>
|
||||
public class PlayerTeam
|
||||
{
|
||||
/// <summary>Team internal name (up to 16 chars)</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Display name component (formatted text)</summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Friendly fire is allowed between team members</summary>
|
||||
public bool AllowFriendlyFire { get; set; }
|
||||
|
||||
/// <summary>Team members can see invisible teammates</summary>
|
||||
public bool SeeFriendlyInvisibles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nametag visibility rule.
|
||||
/// Values: "always", "hideForOtherTeams", "hideForOwnTeam", "never"
|
||||
/// </summary>
|
||||
public string NameTagVisibility { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Collision rule.
|
||||
/// Values: "always", "pushOtherTeams", "pushOwnTeam", "never"
|
||||
/// </summary>
|
||||
public string CollisionRule { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Team color as ChatFormatting enum ordinal (-1 = RESET/none,
|
||||
/// 0–15 = BLACK … WHITE).
|
||||
/// </summary>
|
||||
public int Color { get; set; } = -1;
|
||||
|
||||
/// <summary>Prefix displayed before member names (formatted text)</summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Suffix displayed after member names (formatted text)</summary>
|
||||
public string Suffix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Current set of player / entity names on this team</summary>
|
||||
public HashSet<string> Members { get; } = new(System.StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,9 @@ namespace MinecraftClient.Mapping
|
|||
{
|
||||
/// <summary>
|
||||
/// The chunks contained into the Minecraft world
|
||||
/// Tuple<int, int>: Tuple<chunkX, chunkZ>
|
||||
/// (int ChunkX, int ChunkZ): chunkX, chunkZ
|
||||
/// </summary>
|
||||
private ConcurrentDictionary<Tuple<int, int>, ChunkColumn> chunks = new();
|
||||
private ConcurrentDictionary<(int ChunkX, int ChunkZ), ChunkColumn> chunks = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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<int, int> chunkCoord = new(chunkX, chunkZ);
|
||||
var chunkCoord = (chunkX, chunkZ);
|
||||
if (value is null)
|
||||
chunks.TryRemove(chunkCoord, out _);
|
||||
else
|
||||
|
|
@ -361,7 +361,7 @@ namespace MinecraftClient.Mapping
|
|||
/// <param name="loadCompleted">Whether the ChunkColumn has been fully loaded</param>
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -44,10 +44,15 @@ namespace MinecraftClient
|
|||
|
||||
private readonly Queue<Action> threadTasks = new();
|
||||
private readonly Lock threadTasksLock = new();
|
||||
private readonly Lock recipeBookLock = new();
|
||||
private readonly Lock achievementsLock = new();
|
||||
|
||||
private readonly List<ChatBot> bots = new();
|
||||
private static readonly List<ChatBot> botsOnHold = new();
|
||||
private static readonly Dictionary<int, Container> inventories = new();
|
||||
private readonly Dictionary<string, RecipeBookRecipeEntry> unlockedRecipes = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Achievement> achievements = new(StringComparer.Ordinal);
|
||||
private string? activeAdvancementTab;
|
||||
|
||||
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new();
|
||||
private readonly List<string> registeredServerPluginChannels = new();
|
||||
|
|
@ -105,6 +110,12 @@ namespace MinecraftClient
|
|||
|
||||
// player effects
|
||||
private readonly Dictionary<Effects, EffectData> playerEffects = new();
|
||||
|
||||
// player attributes (e.g., block_break_speed, mining_efficiency, submerged_mining_speed)
|
||||
private readonly Dictionary<string, double> playerAttributes = new();
|
||||
|
||||
// scoreboard teams (key = team name)
|
||||
private readonly Dictionary<string, PlayerTeam> teams = new(StringComparer.Ordinal);
|
||||
|
||||
// Sneaking
|
||||
public bool IsSneaking { get; set; } = false;
|
||||
|
|
@ -156,6 +167,30 @@ namespace MinecraftClient
|
|||
return new Dictionary<Effects, EffectData>(playerEffects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of all known scoreboard teams.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary mapping team name to <see cref="PlayerTeam"/></returns>
|
||||
public Dictionary<string, PlayerTeam> GetTeams()
|
||||
{
|
||||
lock (teams)
|
||||
return new Dictionary<string, PlayerTeam>(teams, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the team that contains the given player/entity name, or <c>null</c> if not found.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all unlocked recipe book recipe identifiers.
|
||||
/// </summary>
|
||||
/// <returns>Unlocked recipe identifiers sorted alphabetically</returns>
|
||||
public RecipeBookRecipeEntry[] GetUnlockedRecipes()
|
||||
{
|
||||
lock (recipeBookLock)
|
||||
{
|
||||
return unlockedRecipes.Values.OrderBy(static recipe => recipe.CommandId, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all achievements/advancements known to the client.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of all achievements</returns>
|
||||
public Achievement[] GetAchievements()
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
return [.. achievements.Values];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only completed achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of completed achievements</returns>
|
||||
public Achievement[] GetUnlockedAchievements()
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
return achievements.Values.Where(static a => a.IsCompleted).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only incomplete achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of locked achievements</returns>
|
||||
public Achievement[] GetLockedAchievements()
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
return achievements.Values.Where(static a => !a.IsCompleted).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all Entities
|
||||
/// </summary>
|
||||
|
|
@ -1404,6 +1488,22 @@ namespace MinecraftClient
|
|||
return GetInventory(0)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the currently active inventory if it supports recipe book crafting.
|
||||
/// </summary>
|
||||
/// <returns>Active recipe book inventory, or null if the active inventory does not support recipe book crafting</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a set of online player names
|
||||
/// </summary>
|
||||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change active slot in the player inventory
|
||||
/// </summary>
|
||||
|
|
@ -2752,6 +2912,31 @@ namespace MinecraftClient
|
|||
|
||||
return handler.SendRenameItem(itemName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a recipe book craft request for the currently active crafting inventory.
|
||||
/// </summary>
|
||||
/// <param name="recipeId">Recipe identifier to craft</param>
|
||||
/// <param name="makeAll">True to craft as many items as possible</param>
|
||||
/// <returns>True if the packet was sent</returns>
|
||||
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
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity velocity update is received.
|
||||
/// </summary>
|
||||
/// <param name="entityID">Entity ID</param>
|
||||
/// <param name="velocityX">Velocity on X axis (blocks/tick)</param>
|
||||
/// <param name="velocityY">Velocity on Y axis (blocks/tick)</param>
|
||||
/// <param name="velocityZ">Velocity on Z axis (blocks/tick)</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key when available, otherwise null</param>
|
||||
/// <param name="location">Sound location when available</param>
|
||||
/// <param name="category">Sound category id from packet</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="entityID">Source entity id for entity sound packets, if any</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when received entity properties from server.
|
||||
/// </summary>
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received. Updates the internal team state and notifies bots.
|
||||
/// </summary>
|
||||
public void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> 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));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -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<Achievement> added, IReadOnlyList<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute whether an achievement is completed based on AND-of-ORs requirements.
|
||||
/// </summary>
|
||||
private static bool ComputeAchievementCompleted(IReadOnlyList<IReadOnlyList<string>> requirements, IReadOnlyDictionary<string, bool> criteria)
|
||||
{
|
||||
if (requirements.Count == 0)
|
||||
return true;
|
||||
|
||||
foreach (IReadOnlyList<string> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
<ItemGroup>
|
||||
<EmbeddedResource Include="Physics\BlockShapeData.json" LogicalName="BlockShapeData.json" />
|
||||
<EmbeddedResource Include="Mcp\Prompts\MccMcpOperatorPrompt.md" LogicalName="MccMcpOperatorPrompt.md" />
|
||||
<EmbeddedResource Include="Tui\MinimapBlockColors.json" LogicalName="MinimapBlockColors.json" />
|
||||
<EmbeddedResource Include="Tui\MinimapEntityCategories.json" LogicalName="MinimapEntityCategories.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Protocol\Handlers\Compression\**" />
|
||||
|
|
|
|||
|
|
@ -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<Aabb>();
|
||||
|
||||
private static Dictionary<int, Aabb[]>? stateToShape;
|
||||
private static FrozenDictionary<int, Aabb[]>? stateToShape;
|
||||
private static Dictionary<string, object>? prismarineBlocks;
|
||||
private static Dictionary<int, Aabb[]>? prismarineShapes;
|
||||
|
||||
|
|
@ -136,14 +137,21 @@ namespace MinecraftClient.Physics
|
|||
|
||||
private static void BuildStateMap()
|
||||
{
|
||||
stateToShape = new Dictionary<int, Aabb[]>();
|
||||
var builder = new Dictionary<int, Aabb[]>();
|
||||
|
||||
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<Material, List<(int start, int end)>>();
|
||||
|
|
@ -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<int> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consolonia's Unix.Terminal uses <c>[DllImport("libcoreclr.so")]</c> to reach
|
||||
/// <c>dlopen</c>/<c>dlsym</c> on .NET Core. The library ships a
|
||||
/// <c>SetDllImportResolver</c> that maps <c>libcoreclr.so</c> to the current
|
||||
/// process, but it is compiled under <c>#if NET6_0</c> (exact TFM match) instead
|
||||
/// of <c>NET6_0_OR_GREATER</c>, so it is dead code when the consuming project
|
||||
/// targets net8.0+. On a self-contained single-file publish the physical
|
||||
/// <c>libcoreclr.so</c> does not exist on the search path, causing a
|
||||
/// <c>DllNotFoundException</c> that crashes the TUI.
|
||||
///
|
||||
/// We work around this by registering our own resolver before any Consolonia
|
||||
/// code runs: if any assembly asks for <c>libcoreclr.so</c> we return
|
||||
/// <c>(IntPtr)(-1)</c> which the runtime interprets as "the current process".
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
|||
/// <returns>True if startup can continue; false if config load failed and user chose to exit.</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a failed config load by prompting the user to fix or regenerate the config file.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -411,6 +411,37 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return ReadNextNbt(cache, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public Item ReadNextItemStackTemplate(Queue<byte> 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<StructuredComponent>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single item slot from a cache of bytes and remove it from the cache
|
||||
/// </summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void ReadNextLpVec3(Queue<byte> cache)
|
||||
public (double X, double Y, double Z) ReadNextLpVec3Values(Queue<byte> 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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it.
|
||||
/// </summary>
|
||||
public void ReadNextLpVec3(Queue<byte> cache)
|
||||
{
|
||||
ReadNextLpVec3Values(cache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
bool ClickContainerButton(int windowId, int buttonId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a place recipe packet to the server for the active recipe book container.
|
||||
/// </summary>
|
||||
/// <param name="windowId">Id of the window being clicked</param>
|
||||
/// <param name="recipeId">Recipe identifier to craft</param>
|
||||
/// <param name="makeAll">True to craft as many items as possible</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
|
||||
|
||||
/// <summary>
|
||||
/// Plays animation
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="onGround">TRUE if on ground</param>
|
||||
void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround);
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity velocity update packet is received.
|
||||
/// Velocity values are in blocks per tick.
|
||||
/// </summary>
|
||||
/// <param name="entityID">Entity ID</param>
|
||||
/// <param name="velocityX">Velocity X</param>
|
||||
/// <param name="velocityY">Velocity Y</param>
|
||||
/// <param name="velocityZ">Velocity Z</param>
|
||||
void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ);
|
||||
|
||||
/// <summary>
|
||||
/// Called when additional properties have been received for an entity
|
||||
/// </summary>
|
||||
|
|
@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="affectedBlocks">Amount of affected blocks</param>
|
||||
void OnExplosion(Location location, float strength, int affectedBlocks);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key if available, otherwise null</param>
|
||||
/// <param name="location">Sound location for world sounds, or null if unavailable</param>
|
||||
/// <param name="category">Sound category id</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="entityID">Source entity id for entity-sound packets, if any</param>
|
||||
void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player's game mode has changed
|
||||
/// </summary>
|
||||
|
|
@ -468,6 +489,23 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -524,6 +562,33 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
public void SetCanSendMessage(bool canSendMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are added or replaced.
|
||||
/// </summary>
|
||||
/// <param name="recipes">Recipe entries to add</param>
|
||||
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
|
||||
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are removed.
|
||||
/// </summary>
|
||||
/// <param name="recipeIds">Recipe identifiers to remove</param>
|
||||
public void OnRecipeBookRemove(string[] recipeIds);
|
||||
|
||||
/// <summary>
|
||||
/// Called when achievement/advancement data is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="added">Achievements that were added or updated</param>
|
||||
/// <param name="removedIds">IDs of achievements that were removed</param>
|
||||
/// <param name="reset">True if all existing state should be cleared before applying</param>
|
||||
public void OnAchievementsUpdate(IReadOnlyList<Achievement> added, IReadOnlyList<string> removedIds, bool reset);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server selects an advancement tab.
|
||||
/// </summary>
|
||||
/// <param name="tabId">The tab identifier, or null if no tab is selected</param>
|
||||
public void OnSelectAdvancementTab(string? tabId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a click container button packet to the server.
|
||||
/// Used for Enchanting table, Lectern, stone cutter and loom
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <paramref name="protocolVersion"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a network protocol version number to human-readable Minecraft version number
|
||||
/// </summary>
|
||||
|
|
|
|||
119
MinecraftClient/Protocol/ServerStatusDisplay.cs
Normal file
119
MinecraftClient/Protocol/ServerStatusDisplay.cs
Normal file
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
MinecraftClient/Protocol/ServerStatusInfo.cs
Normal file
30
MinecraftClient/Protocol/ServerStatusInfo.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the structured result of a Minecraft server status (SLP) ping,
|
||||
/// including MOTD, player counts, sample player list, version, and favicon.
|
||||
/// </summary>
|
||||
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<SamplePlayer> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
MinecraftClient/RecipeBookRecipeEntry.cs
Normal file
4
MinecraftClient/RecipeBookRecipeEntry.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace MinecraftClient
|
||||
{
|
||||
public readonly record struct RecipeBookRecipeEntry(string CommandId, string DisplayText);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -311,6 +311,21 @@ You can use "/fish" to control the bot manually.
|
|||
<data name="ChatBot.AutoFishing.Hook_Threshold" xml:space="preserve">
|
||||
<value>A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Enable_Velocity_Detection" xml:space="preserve">
|
||||
<value>Enable fish bite detection using fishing bobber velocity packets.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Velocity_Hook_Threshold" xml:space="preserve">
|
||||
<value>Velocity Y threshold (blocks/tick). Values below this are treated as a bite. Keep this value negative.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Enable_Sound_Detection" xml:space="preserve">
|
||||
<value>Enable fish bite detection using splash sounds near the fishing bobber.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Sound_Distance" xml:space="preserve">
|
||||
<value>Maximum distance (blocks) between splash sound and bobber to treat it as a bite.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Detection_Warmup" xml:space="preserve">
|
||||
<value>Delay (seconds) after bobber spawn before bite detection starts. Helps ignore cast-entry splash/motion.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Log_Fish_Bobber" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
|
|
@ -393,6 +408,12 @@ For Discord message formatting, check the following: https://mccteam.github.io/r
|
|||
<data name="ChatBot.DiscordBridge.AllowOtherBotMessages" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="ChatBot.DiscordBridge.RelayAllMessages" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="ChatBot.DiscordBridge.MessageAggregationInterval" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="ChatBot.Farmer" xml:space="preserve">
|
||||
<value>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.</value>
|
|||
<data name="Console.General.ConsoleColorMode" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Console.General.Display_Icon_Banner" xml:space="preserve">
|
||||
<value>Whether to display the MCC startup icon banner.</value>
|
||||
</data>
|
||||
<data name="Console.General.Display_Input" xml:space="preserve">
|
||||
<value>You can use "Ctrl+P" to print out the current input and cursor position.</value>
|
||||
</data>
|
||||
<data name="Console.General.History_Input_Records" xml:space="preserve">
|
||||
<value>Maximum number of input history records to keep.</value>
|
||||
</data>
|
||||
<data name="Console.General.TUI_Log_Scrollback" xml:space="preserve">
|
||||
<value>Maximum log lines kept in TUI mode scrollback. Set to 0 for automatic.</value>
|
||||
</data>
|
||||
<data name="Head" xml:space="preserve">
|
||||
<value>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
|
|||
<data name="Main.Advanced.enable_sentry" xml:space="preserve">
|
||||
<value>Set to false to opt-out of Sentry error logging.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap" xml:space="preserve">
|
||||
<value>Settings for the TUI minimap overlay that shows terrain and entities.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Enabled" xml:space="preserve">
|
||||
<value>Whether the minimap is visible on startup in TUI mode.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Zoom" xml:space="preserve">
|
||||
<value>Blocks per pixel, 1-16. 1 = closest (1:1), 16 = farthest (16 blocks per pixel).</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Width" xml:space="preserve">
|
||||
<value>Map width in pixels (characters). Range 10-120, default 40.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Height" xml:space="preserve">
|
||||
<value>Map height in pixels (must be even, uses half-block chars). Range 4-80, default 40.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Position" xml:space="preserve">
|
||||
<value>Minimap position: "top_left", "top_right", "center", "bottom_left", or "bottom_right".</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowPlayerNames" xml:space="preserve">
|
||||
<value>Show player names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowHostileNames" xml:space="preserve">
|
||||
<value>Show hostile mob names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowNeutralNames" xml:space="preserve">
|
||||
<value>Show neutral mob names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowPassiveNames" xml:space="preserve">
|
||||
<value>Show passive mob names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.RefreshInterval" xml:space="preserve">
|
||||
<value>Minimap refresh interval in milliseconds (100-5000).</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.CaveMode" xml:space="preserve">
|
||||
<value>Cave rendering mode: "auto" (detect ceiling), "on" (always cave view), "off" (always surface view).</value>
|
||||
</data>
|
||||
<data name="Main.General.AuthlibUser" xml:space="preserve">
|
||||
<value>Yggdrasil authlib multi-user selection.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -437,6 +437,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dropped low durability {0} from slot {1}..
|
||||
/// </summary>
|
||||
internal static string bot_autodig_drop_low_durability {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autodig.drop_low_durability", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The block currently pointed to is not in the allowed list..
|
||||
/// </summary>
|
||||
|
|
@ -473,6 +482,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Switch to {0} from slot {1}..
|
||||
/// </summary>
|
||||
internal static string bot_autodig_switch {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autodig.switch", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Added item {0}.
|
||||
/// </summary>
|
||||
|
|
@ -879,6 +897,24 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Waiting {0:0.000} seconds before reconnecting... ({1} retries left).
|
||||
/// </summary>
|
||||
internal static string bot_autoRelog_wait_with_retries {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autoRelog.wait_with_retries", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to unlimited.
|
||||
/// </summary>
|
||||
internal static string bot_autoRelog_retries_unlimited {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autoRelog.retries_unlimited", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to File not found: '{0}'.
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Converting session cache from disk: {0}.
|
||||
/// </summary>
|
||||
|
|
@ -3558,6 +3666,51 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to quickly enable recommended features..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Available quick actions:.
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_list_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.list.header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to tui: set [Console.General] ConsoleMode = "tui" for the next restart..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_list_tui {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.list.tui", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_tui_already_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.tui.already_enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_tui_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.tui.enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Display Health and Food saturation..
|
||||
/// </summary>
|
||||
|
|
@ -4254,6 +4407,87 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("cmd.nameitem.successful", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to send recipe book craft request for {0}..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_craft_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.craft.failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Requested recipe {0}..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_craft_sent {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.craft.sent", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Requested recipe {0} with craft-all..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_craftall_sent {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.craftall.sent", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to List unlocked recipe book recipes and craft them through the active recipe book inventory..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unlocked recipe book recipes.
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_list {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.list", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_no_active_inventory {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.no.active.inventory", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No unlocked recipe book recipes are currently tracked..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_no_recipes {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.no.recipes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The recipe identifier cannot be empty..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_recipe_id_empty {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.recipe.id.empty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Recipe book crafting is only supported on Minecraft 1.13 and newer..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_unsupported {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.unsupported", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to restart and reconnect to the server..
|
||||
|
|
@ -4462,6 +4696,51 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to List all scoreboard teams and their members.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No teams are currently tracked.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_no_teams {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.no_teams", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Team '{0}' (display: {1}, ...).
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Members ({0}): {1}.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_members {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_members", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No members.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_no_members {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_no_members", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Place a block or open chest.
|
||||
/// </summary>
|
||||
|
|
@ -5534,6 +5813,42 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string mcc_console_mode_tui_recommendation {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.console_mode_tui_recommendation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to MCC encountered a problem while starting TUI mode..
|
||||
/// </summary>
|
||||
internal static string mcc_tui_startup_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.tui_startup_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string mcc_tui_startup_fallback_classic {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.tui_startup_fallback_classic", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please report this issue to the MCC Team..
|
||||
/// </summary>
|
||||
internal static string mcc_report_issue {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.report_issue", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to To sign in, open {0} in your browser and enter the code: {1}.
|
||||
/// </summary>
|
||||
|
|
@ -5861,6 +6176,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cannot send text: not connected to a server..
|
||||
/// </summary>
|
||||
internal static string mcc_send_text_not_connected {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.send_text_not_connected", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Waiting {0} seconds before restarting....
|
||||
/// </summary>
|
||||
|
|
@ -6799,7 +7123,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("tui.crafting.grid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Starting embedded MCP server....
|
||||
/// </summary>
|
||||
|
|
@ -6808,7 +7132,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.starting", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Embedded MCP server started on {0}.
|
||||
/// </summary>
|
||||
|
|
@ -6817,7 +7141,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.started", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to start embedded MCP server: {0}.
|
||||
/// </summary>
|
||||
|
|
@ -6826,7 +7150,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.start_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Embedded MCP auth token is required but environment variable {0} is empty..
|
||||
/// </summary>
|
||||
|
|
@ -6835,7 +7159,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.missing_auth_token", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Embedded MCP server stopped..
|
||||
/// </summary>
|
||||
|
|
@ -6844,7 +7168,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.stopped", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to stop embedded MCP server cleanly: {0}.
|
||||
/// </summary>
|
||||
|
|
@ -6853,5 +7177,275 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.stop_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Toggle the TUI minimap overlay, or adjust its zoom level..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap enabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap disabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_disabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.disabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap zoom set to {0}:1 (blocks per pixel)..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_zoom_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.zoom_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current minimap zoom: {0}:1 blocks/px (range 1-{1})..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_zoom_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.zoom_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The minimap command is only available in TUI mode..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_tui_only {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.tui_only", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hostile.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_hostile {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.hostile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passive.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_passive {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.passive", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Neutral.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_neutral {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.neutral", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Player.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_player {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.player", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_status {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_status", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to All entity name labels enabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_all_on {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_all_on", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to All entity name labels disabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_all_off {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_all_off", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} name display: {1}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_cat {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_cat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} name display set to {1}..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_cat_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_cat_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current minimap position: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_position_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.position_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap position set to: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_position_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.position_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current cave mode: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_cave_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.cave_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cave mode set to: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_cave_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.cave_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to list achievements/advancements from the server..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No achievements/advancements received yet..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_none {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.none", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No completed achievements/advancements..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_none_unlocked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.none_unlocked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No incomplete achievements/advancements..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_none_locked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.none_locked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Achievements/Advancements:.
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Completed achievements/advancements:.
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_header_unlocked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.header_unlocked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Incomplete achievements/advancements:.
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_header_locked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.header_locked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [DONE].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_done {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.done", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [TODO].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_todo {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.todo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} {1} ({2}) [{3}].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_entry_titled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.entry_titled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} {1} [{2}].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_entry {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.entry", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@
|
|||
<data name="bot.autodig.no_inv_handle" xml:space="preserve">
|
||||
<value>Inventory handling is not enabled. Unable to switch tools automatically.</value>
|
||||
</data>
|
||||
<data name="bot.autodig.drop_low_durability" xml:space="preserve">
|
||||
<value>Dropped low durability {0} from slot {1}.</value>
|
||||
</data>
|
||||
<data name="bot.autodig.start" xml:space="preserve">
|
||||
<value>Automatic digging has started.</value>
|
||||
</data>
|
||||
|
|
@ -252,6 +255,9 @@
|
|||
<data name="bot.autodig.stop" xml:space="preserve">
|
||||
<value>Auto-digging has been stopped.</value>
|
||||
</data>
|
||||
<data name="bot.autodig.switch" xml:space="preserve">
|
||||
<value>Switch to {0} from slot {1}.</value>
|
||||
</data>
|
||||
<data name="bot.autoDrop.added" xml:space="preserve">
|
||||
<value>Added item {0}</value>
|
||||
</data>
|
||||
|
|
@ -388,6 +394,12 @@
|
|||
<data name="bot.autoRelog.wait" xml:space="preserve">
|
||||
<value>Waiting {0:0.000} seconds before reconnecting...</value>
|
||||
</data>
|
||||
<data name="bot.autoRelog.wait_with_retries" xml:space="preserve">
|
||||
<value>Waiting {0:0.000} seconds before reconnecting... ({1} retries left)</value>
|
||||
</data>
|
||||
<data name="bot.autoRelog.retries_unlimited" xml:space="preserve">
|
||||
<value>unlimited</value>
|
||||
</data>
|
||||
<data name="bot.autoRespond.file_not_found" xml:space="preserve">
|
||||
<value>File not found: '{0}'</value>
|
||||
</data>
|
||||
|
|
@ -830,6 +842,42 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file
|
|||
<data name="botname.TestBot" xml:space="preserve">
|
||||
<value>TestBot</value>
|
||||
</data>
|
||||
<data name="mcc.banner.classic" xml:space="preserve">
|
||||
<value>Minecraft Console Client v{0} - for MC {1} to {2} - {3}</value>
|
||||
</data>
|
||||
<data name="mcc.banner.label_mc_versions" xml:space="preserve">
|
||||
<value>Supported MC Versions:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_server" xml:space="preserve">
|
||||
<value>Server:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_version" xml:space="preserve">
|
||||
<value>Version:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_protocol" xml:space="preserve">
|
||||
<value>Protocol: {0}</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_players" xml:space="preserve">
|
||||
<value>Players:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_ping" xml:space="preserve">
|
||||
<value>Ping:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_ping_ms" xml:space="preserve">
|
||||
<value>{0} ms</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_connecting_as" xml:space="preserve">
|
||||
<value>Connecting as:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_online" xml:space="preserve">
|
||||
<value>Online Players:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.sample_more" xml:space="preserve">
|
||||
<value>... +{0}</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.version_upgrade" xml:space="preserve">
|
||||
<value>Server reported protocol {0} ({1}), upgraded to {2} ({3}) for best compatibility</value>
|
||||
</data>
|
||||
<data name="cache.converting" xml:space="preserve">
|
||||
<value>Converting session cache from disk: {0}</value>
|
||||
</data>
|
||||
|
|
@ -1249,6 +1297,21 @@ Change EnableEmoji=false in the settings if the display is confusing.</value>
|
|||
<data name="cmd.effects.none" xml:space="preserve">
|
||||
<value>No active effects.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.desc" xml:space="preserve">
|
||||
<value>try a recommended feature.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.list.header" xml:space="preserve">
|
||||
<value>Available tryouts:</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.list.tui" xml:space="preserve">
|
||||
<value>tui: set [Console.General] ConsoleMode = "tui" for the next restart.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.tui.already_enabled" xml:space="preserve">
|
||||
<value>[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.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.tui.enabled" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="cmd.health.desc" xml:space="preserve">
|
||||
<value>Display Health and Food saturation.</value>
|
||||
</data>
|
||||
|
|
@ -1511,6 +1574,21 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="cmd.tps.desc" xml:space="preserve">
|
||||
<value>Display server current tps (tick per second). May not be accurate</value>
|
||||
</data>
|
||||
<data name="cmd.teams.desc" xml:space="preserve">
|
||||
<value>List all scoreboard teams and their members.</value>
|
||||
</data>
|
||||
<data name="cmd.teams.no_teams" xml:space="preserve">
|
||||
<value>No teams are currently tracked.</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_header" xml:space="preserve">
|
||||
<value>Team '{0}' (display: {1}, color: {2}, prefix: '{3}', suffix: '{4}', nameTagVisibility: {5}, collisionRule: {6}, friendlyFire: {7}, seeInvisibles: {8})</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_members" xml:space="preserve">
|
||||
<value> Members ({0}): {1}</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_no_members" xml:space="preserve">
|
||||
<value> No members.</value>
|
||||
</data>
|
||||
<data name="cmd.useblock.desc" xml:space="preserve">
|
||||
<value>Place a block or open chest</value>
|
||||
</data>
|
||||
|
|
@ -1863,6 +1941,18 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="mcc.connecting" xml:space="preserve">
|
||||
<value>Connecting to {0}...</value>
|
||||
</data>
|
||||
<data name="mcc.console_mode_tui_recommendation" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="mcc.tui_startup_failed" xml:space="preserve">
|
||||
<value>MCC encountered a problem while starting TUI mode.</value>
|
||||
</data>
|
||||
<data name="mcc.tui_startup_fallback_classic" xml:space="preserve">
|
||||
<value>As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC.</value>
|
||||
</data>
|
||||
<data name="mcc.report_issue" xml:space="preserve">
|
||||
<value>Please report this issue to the MCC Team.</value>
|
||||
</data>
|
||||
<data name="mcc.device_code_prompt" xml:space="preserve">
|
||||
<value>To sign in, open {0} in your browser and enter the code: §e{1}</value>
|
||||
</data>
|
||||
|
|
@ -1974,6 +2064,9 @@ Type '{0}quit' to leave the server.</value>
|
|||
<data name="mcc.restart" xml:space="preserve">
|
||||
<value>Restarting Minecraft Console Client...</value>
|
||||
</data>
|
||||
<data name="mcc.send_text_not_connected" xml:space="preserve">
|
||||
<value>Cannot send text: not connected to a server.</value>
|
||||
</data>
|
||||
<data name="mcc.restart_delay" xml:space="preserve">
|
||||
<value>Waiting {0} seconds before restarting...</value>
|
||||
</data>
|
||||
|
|
@ -1988,10 +2081,10 @@ MCC is running with default settings.</value>
|
|||
<value>Server is in offline mode.</value>
|
||||
</data>
|
||||
<data name="mcc.server_protocol" xml:space="preserve">
|
||||
<value>Server version : {0} (protocol v{1})</value>
|
||||
<value>Server version: {0} (protocol v{1})</value>
|
||||
</data>
|
||||
<data name="mcc.server_version" xml:space="preserve">
|
||||
<value>Server version : </value>
|
||||
<value>Server version: </value>
|
||||
</data>
|
||||
<data name="mcc.session" xml:space="preserve">
|
||||
<value>Checking Session...</value>
|
||||
|
|
@ -2151,6 +2244,33 @@ Logging in...</value>
|
|||
<data name="cmd.nameitem.desc" xml:space="preserve">
|
||||
<value>Set an item name when an Anvil inventory is active and the item is in the first slot.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.craft.failed" xml:space="preserve">
|
||||
<value>Failed to send recipe book craft request for {0}.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.craft.sent" xml:space="preserve">
|
||||
<value>Requested recipe {0}.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.craftall.sent" xml:space="preserve">
|
||||
<value>Requested recipe {0} with craft-all.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.desc" xml:space="preserve">
|
||||
<value>List unlocked recipe book recipes and craft them through the active recipe book inventory.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.list" xml:space="preserve">
|
||||
<value>Unlocked recipe book recipes</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.no.active.inventory" xml:space="preserve">
|
||||
<value>You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.no.recipes" xml:space="preserve">
|
||||
<value>No unlocked recipe book recipes are currently tracked.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.recipe.id.empty" xml:space="preserve">
|
||||
<value>The recipe identifier cannot be empty.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.unsupported" xml:space="preserve">
|
||||
<value>Recipe book crafting is only supported on Minecraft 1.13 and newer.</value>
|
||||
</data>
|
||||
<data name="bot.antiafk.may.not.move" xml:space="preserve">
|
||||
<value>Bot movement lock is held by bot {0}, so the Anti AFK bot might not move!</value>
|
||||
</data>
|
||||
|
|
@ -2413,4 +2533,94 @@ see item details.</value>
|
|||
<data name="bot.mcpserver.stop_failed" xml:space="preserve">
|
||||
<value>Failed to stop embedded MCP server cleanly: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.desc" xml:space="preserve">
|
||||
<value>Toggle the TUI minimap overlay, or adjust its zoom level.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.enabled" xml:space="preserve">
|
||||
<value>Minimap enabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.disabled" xml:space="preserve">
|
||||
<value>Minimap disabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.zoom_set" xml:space="preserve">
|
||||
<value>Minimap zoom set to {0}:1 (blocks per pixel).</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.zoom_current" xml:space="preserve">
|
||||
<value>Current minimap zoom: {0}:1 blocks/px (range 1-{1}).</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.tui_only" xml:space="preserve">
|
||||
<value>The minimap command is only available in TUI mode.</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.hostile" xml:space="preserve">
|
||||
<value>Hostile</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.passive" xml:space="preserve">
|
||||
<value>Passive</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.neutral" xml:space="preserve">
|
||||
<value>Neutral</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.player" xml:space="preserve">
|
||||
<value>Player</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_status" xml:space="preserve">
|
||||
<value>Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_all_on" xml:space="preserve">
|
||||
<value>All entity name labels enabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_all_off" xml:space="preserve">
|
||||
<value>All entity name labels disabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_cat" xml:space="preserve">
|
||||
<value>{0} name display: {1}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_cat_set" xml:space="preserve">
|
||||
<value>{0} name display set to {1}.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.position_current" xml:space="preserve">
|
||||
<value>Current minimap position: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.position_set" xml:space="preserve">
|
||||
<value>Minimap position set to: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.cave_current" xml:space="preserve">
|
||||
<value>Current cave mode: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.cave_set" xml:space="preserve">
|
||||
<value>Cave mode set to: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.desc" xml:space="preserve">
|
||||
<value>list achievements/advancements from the server.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.none" xml:space="preserve">
|
||||
<value>No achievements/advancements received yet.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.none_unlocked" xml:space="preserve">
|
||||
<value>No completed achievements/advancements.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.none_locked" xml:space="preserve">
|
||||
<value>No incomplete achievements/advancements.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.header" xml:space="preserve">
|
||||
<value>Achievements/Advancements:</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.header_unlocked" xml:space="preserve">
|
||||
<value>Completed achievements/advancements:</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.header_locked" xml:space="preserve">
|
||||
<value>Incomplete achievements/advancements:</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.done" xml:space="preserve">
|
||||
<value>[DONE]</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.todo" xml:space="preserve">
|
||||
<value>[TODO]</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.entry_titled" xml:space="preserve">
|
||||
<value>{0} {1} ({2}) [{3}]</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.entry" xml:space="preserve">
|
||||
<value>{0} {1} [{2}]</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -205,6 +205,29 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="entity">Entity with updated location</param>
|
||||
public virtual void OnEntityMove(Entity entity) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a tracked entity receives a velocity update packet.
|
||||
/// Velocity is expressed in blocks per tick.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity with updated velocity</param>
|
||||
/// <param name="velocityX">Velocity on X axis (blocks/tick)</param>
|
||||
/// <param name="velocityY">Velocity on Y axis (blocks/tick)</param>
|
||||
/// <param name="velocityZ">Velocity on Z axis (blocks/tick)</param>
|
||||
public virtual void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// The sound name is null when the protocol provides only a registry id.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key when available, otherwise null</param>
|
||||
/// <param name="location">Sound position when available</param>
|
||||
/// <param name="category">Sound category id from packet</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="sourceEntity">Source entity for entity-sound packets when tracked</param>
|
||||
public virtual void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch,
|
||||
Entity? sourceEntity) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity rotates
|
||||
/// </summary>
|
||||
|
|
@ -367,6 +390,23 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
public virtual void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int value, int numberFormat) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
public virtual void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -520,6 +560,14 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="block">The block</param>
|
||||
public virtual void OnBlockChange(Location location, Block block) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when achievement/advancement data is updated.
|
||||
/// </summary>
|
||||
/// <param name="updated">Achievements that were added or updated</param>
|
||||
/// <param name="removedIds">IDs of achievements that were removed</param>
|
||||
/// <param name="reset">Whether the achievement state was fully reset before this update</param>
|
||||
public virtual void OnAchievementUpdate(IReadOnlyList<Achievement> updated, IReadOnlyList<string> 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
|
|||
/// <param name="direction">Example: if your player is under a block that is being destroyed, use Down</param>
|
||||
/// <param name="swingArms">Also perform the "arm swing" animation</param>
|
||||
/// <param name="lookAtBlock">Also look at the block before digging</param>
|
||||
protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true)
|
||||
/// <param name="duration">Dig duration in seconds. 0 = auto-compute for survival, or instant for creative</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1126,6 +1175,33 @@ namespace MinecraftClient.Scripting
|
|||
return Handler.GetEntities();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of all achievements</returns>
|
||||
protected Achievement[] GetAchievements()
|
||||
{
|
||||
return Handler.GetAchievements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only completed achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of unlocked achievements</returns>
|
||||
protected Achievement[] GetUnlockedAchievements()
|
||||
{
|
||||
return Handler.GetUnlockedAchievements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only incomplete achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of locked achievements</returns>
|
||||
protected Achievement[] GetLockedAchievements()
|
||||
{
|
||||
return Handler.GetLockedAchievements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all players Latency
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
125
MinecraftClient/Tui/IconGridBuilder.cs
Normal file
125
MinecraftClient/Tui/IconGridBuilder.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> _logLines = new();
|
||||
private readonly ObservableCollection<Control> _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<CommandSuggestion>();
|
||||
|
|
@ -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<Panel?>(() => 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
178
MinecraftClient/Tui/MccBannerPanelBuilder.cs
Normal file
178
MinecraftClient/Tui/MccBannerPanelBuilder.cs
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
3552
MinecraftClient/Tui/MinimapBlockColors.json
Normal file
3552
MinecraftClient/Tui/MinimapBlockColors.json
Normal file
File diff suppressed because it is too large
Load diff
191
MinecraftClient/Tui/MinimapColorMap.cs
Normal file
191
MinecraftClient/Tui/MinimapColorMap.cs
Normal file
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Material, Color> ColorTable;
|
||||
private static readonly FrozenSet<Material> FullyTransparentMats;
|
||||
private static readonly FrozenSet<Material> WaterMats;
|
||||
private static readonly FrozenSet<Material> IceMats;
|
||||
|
||||
static MinimapColorMap()
|
||||
{
|
||||
var colors = new Dictionary<Material, Color>();
|
||||
var transparent = new HashSet<Material>();
|
||||
var water = new HashSet<Material>();
|
||||
var ice = new HashSet<Material>();
|
||||
|
||||
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<Material>(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<Material>(item.GetString(), out var mat))
|
||||
transparent.Add(mat);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("water", out var waterEl))
|
||||
{
|
||||
foreach (var item in waterEl.EnumerateArray())
|
||||
{
|
||||
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||
water.Add(mat);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("ice", out var iceEl))
|
||||
{
|
||||
foreach (var item in iceEl.EnumerateArray())
|
||||
{
|
||||
if (Enum.TryParse<Material>(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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1267
MinecraftClient/Tui/MinimapControl.cs
Normal file
1267
MinecraftClient/Tui/MinimapControl.cs
Normal file
File diff suppressed because it is too large
Load diff
167
MinecraftClient/Tui/MinimapEntityCategories.json
Normal file
167
MinecraftClient/Tui/MinimapEntityCategories.json
Normal file
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
178
MinecraftClient/Tui/MinimapEntityClassifier.cs
Normal file
178
MinecraftClient/Tui/MinimapEntityClassifier.cs
Normal file
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<EntityType, MobCategory> CategoryTable;
|
||||
|
||||
static MinimapEntityClassifier()
|
||||
{
|
||||
var table = new Dictionary<EntityType, MobCategory>();
|
||||
|
||||
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<EntityType, MobCategory> 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<EntityType>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
196
MinecraftClient/Tui/ServerStatusPanelBuilder.cs
Normal file
196
MinecraftClient/Tui/ServerStatusPanelBuilder.cs
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
114
MinecraftClient/Tui/TuiTooltipService.cs
Normal file
114
MinecraftClient/Tui/TuiTooltipService.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global tooltip that floats above all TUI content.
|
||||
/// Owned by MainTuiView, used by minimap / chat / other components.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <param name="mouseX">Global X of the mouse cursor.</param>
|
||||
/// <param name="mouseY">Global Y of the mouse cursor.</param>
|
||||
/// <param name="preferRight">
|
||||
/// 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.
|
||||
/// </param>
|
||||
public void Show(double mouseX, double mouseY, IReadOnlyList<TuiTooltipLine> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ MCC.LoadBot(new PacketCadenceCaptureBot());
|
|||
|
||||
//MCCScript Extensions
|
||||
|
||||
using System.Threading;
|
||||
|
||||
public class PacketCadenceCaptureBot : ChatBot
|
||||
{
|
||||
private const int CaptureDurationSeconds = 5;
|
||||
|
|
|
|||
20
README.md
20
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 📚
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
85
docs/.vuepress/public/install.ps1
Normal file
85
docs/.vuepress/public/install.ps1
Normal file
|
|
@ -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"
|
||||
106
docs/.vuepress/public/install.sh
Normal file
106
docs/.vuepress/public/install.sh
Normal file
|
|
@ -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"
|
||||
|
|
@ -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.
|
||||
|
||||
<div class="custom-container note"><p class="custom-container-title">Note</p>
|
||||
|
||||
|
|
@ -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:**
|
||||
|
|
|
|||
|
|
@ -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<Achievement> updated, IReadOnlyList<string> 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<Achievement> updated, IReadOnlyList<string> 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<string> 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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -219,6 +219,54 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
|
|||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>achievement</code></summary>
|
||||
|
||||
- **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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>bed</code></summary>
|
||||
|
||||
|
|
@ -650,6 +698,79 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
|
|||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>recipebook</code></summary>
|
||||
|
||||
- **Description:**
|
||||
|
||||
List unlocked recipe book entries and ask the server to place one of them into the active crafting inventory.
|
||||
|
||||
<div class="custom-container note"><p class="custom-container-title">Note</p>
|
||||
|
||||
**You need to have [Inventory Handling](configuration.md#inventoryhandling) enabled in order for this command to work.**
|
||||
|
||||
</div>
|
||||
|
||||
<div class="custom-container note"><p class="custom-container-title">Note</p>
|
||||
|
||||
**`craft` and `craftall` need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.**
|
||||
|
||||
</div>
|
||||
|
||||
<div class="custom-container warning"><p class="custom-container-title">Warning</p>
|
||||
|
||||
**Recipe book crafting is supported on Minecraft `1.13+`.**
|
||||
|
||||
</div>
|
||||
|
||||
`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 <recipe id>
|
||||
```
|
||||
|
||||
```
|
||||
/recipebook craftall <recipe id>
|
||||
```
|
||||
|
||||
- **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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>connect</code></summary>
|
||||
|
||||
|
|
@ -832,6 +953,30 @@ In scripts and remote control, no slash is needed to perform the command, eg. `q
|
|||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>teams</code></summary>
|
||||
|
||||
- **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.
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><code>useitem</code></summary>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/<ver>/server-<ver>.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"
|
||||
|
|
|
|||
268
tools/gen_block_color_map.py
Normal file
268
tools/gen_block_color_map.py
Normal file
|
|
@ -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 <decompiled_root>
|
||||
|
||||
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()
|
||||
200
tools/gen_entity_category_map.py
Normal file
200
tools/gen_entity_category_map.py
Normal file
|
|
@ -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 <decompiled_root>
|
||||
|
||||
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()
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" "$@"; }
|
||||
|
|
|
|||
|
|
@ -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=$!
|
||||
|
||||
|
|
|
|||
|
|
@ -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/<version>/.
|
||||
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue