mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge branch 'master' into copilot/implement-automatic-mining-handling
This commit is contained in:
commit
be20b97478
35 changed files with 4467 additions and 2463 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -436,5 +436,8 @@ FodyWeavers.xsd
|
|||
# SpecStory files
|
||||
/.specstory/
|
||||
/.vscode/settings.json
|
||||
|
||||
# Other
|
||||
/Sentry/
|
||||
/downloads/
|
||||
server.pid
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,11 +45,14 @@ 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();
|
||||
|
|
@ -1356,6 +1359,42 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
|
@ -4198,6 +4237,67 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -228,9 +228,26 @@ 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;
|
||||
|
||||
|
|
@ -271,6 +288,13 @@ 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
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
private int currentDimension;
|
||||
private bool isOnlineMode = false;
|
||||
private readonly BlockingCollection<Tuple<int, Queue<byte>>> packetQueue = new();
|
||||
private readonly Dictionary<string, bool> legacyAchievementProgress = new(StringComparer.Ordinal);
|
||||
private float LastYaw, LastPitch;
|
||||
private double lastSentX, lastSentY, lastSentZ;
|
||||
private float lastSentYaw, lastSentPitch;
|
||||
|
|
@ -120,6 +121,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
Tuple<Thread, CancellationTokenSource>? netReader = null; // reader thread
|
||||
readonly ILogger log;
|
||||
readonly RandomNumberGenerator randomGen;
|
||||
private bool legacyAchievementsInitialized;
|
||||
|
||||
public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler,
|
||||
ForgeInfo? forgeInfo, int rawProtocolVersion = 0)
|
||||
|
|
@ -3132,6 +3134,19 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
case PacketTypesIn.RecipeBookSettings:
|
||||
break;
|
||||
|
||||
case PacketTypesIn.Statistics:
|
||||
if (protocolVersion < MC_1_12_Version)
|
||||
HandleLegacyStatistics(packetData);
|
||||
break;
|
||||
|
||||
case PacketTypesIn.Advancements:
|
||||
HandleAdvancements(packetData);
|
||||
break;
|
||||
|
||||
case PacketTypesIn.SelectAdvancementTab:
|
||||
HandleSelectAdvancementTab(packetData);
|
||||
break;
|
||||
|
||||
default:
|
||||
return false; //Ignored packet
|
||||
}
|
||||
|
|
@ -3139,6 +3154,218 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return true; //Packet processed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle the Statistics packet for pre-1.12 legacy achievements.
|
||||
/// </summary>
|
||||
private void HandleLegacyStatistics(Queue<byte> packetData)
|
||||
{
|
||||
int statCount = dataTypes.ReadNextVarInt(packetData);
|
||||
|
||||
for (int i = 0; i < statCount; i++)
|
||||
{
|
||||
string statId = dataTypes.ReadNextString(packetData);
|
||||
int value = dataTypes.ReadNextVarInt(packetData);
|
||||
|
||||
if (statId.StartsWith("achievement.", StringComparison.Ordinal))
|
||||
legacyAchievementProgress[statId] = value > 0;
|
||||
}
|
||||
|
||||
List<Achievement> added = new(LegacyAchievementCatalog.Ids.Count + legacyAchievementProgress.Count);
|
||||
|
||||
foreach (string achievementId in LegacyAchievementCatalog.Ids)
|
||||
added.Add(CreateLegacyAchievement(achievementId, legacyAchievementProgress.TryGetValue(achievementId, out bool completed) && completed));
|
||||
|
||||
foreach (var (achievementId, completed) in legacyAchievementProgress)
|
||||
{
|
||||
if (!LegacyAchievementCatalog.Contains(achievementId))
|
||||
added.Add(CreateLegacyAchievement(achievementId, completed));
|
||||
}
|
||||
|
||||
handler.OnAchievementsUpdate(added, [], reset: !legacyAchievementsInitialized);
|
||||
legacyAchievementsInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle the Advancements packet (1.12+).
|
||||
/// </summary>
|
||||
private void HandleAdvancements(Queue<byte> packetData)
|
||||
{
|
||||
bool reset = dataTypes.ReadNextBool(packetData);
|
||||
|
||||
// --- Added advancements ---
|
||||
int addedCount = dataTypes.ReadNextVarInt(packetData);
|
||||
var added = new List<Achievement>(addedCount);
|
||||
var addedDefinitions = new Dictionary<string, (string? title, string? description, AchievementType type, bool isHidden, List<List<string>> requirements)>(addedCount);
|
||||
|
||||
for (int i = 0; i < addedCount; i++)
|
||||
{
|
||||
string id = dataTypes.ReadNextString(packetData);
|
||||
|
||||
// Parent
|
||||
bool hasParent = dataTypes.ReadNextBool(packetData);
|
||||
if (hasParent)
|
||||
dataTypes.ReadNextString(packetData); // parentId - read and discard
|
||||
|
||||
// Display
|
||||
string? title = null;
|
||||
string? description = null;
|
||||
var type = AchievementType.Task;
|
||||
bool isHidden = false;
|
||||
|
||||
bool hasDisplay = dataTypes.ReadNextBool(packetData);
|
||||
if (hasDisplay)
|
||||
{
|
||||
title = dataTypes.ReadNextChat(packetData);
|
||||
description = dataTypes.ReadNextChat(packetData);
|
||||
dataTypes.ReadNextItemSlot(packetData, itemPalette); // icon - read and discard
|
||||
|
||||
int frameType = dataTypes.ReadNextVarInt(packetData);
|
||||
type = frameType switch
|
||||
{
|
||||
1 => AchievementType.Challenge,
|
||||
2 => AchievementType.Goal,
|
||||
_ => AchievementType.Task
|
||||
};
|
||||
|
||||
int flags = dataTypes.ReadNextInt(packetData);
|
||||
isHidden = (flags & 0x04) != 0;
|
||||
if ((flags & 0x01) != 0)
|
||||
dataTypes.ReadNextString(packetData); // background texture - read and discard
|
||||
|
||||
dataTypes.ReadNextFloat(packetData); // x
|
||||
dataTypes.ReadNextFloat(packetData); // y
|
||||
}
|
||||
|
||||
// Criteria and requirements differ by version
|
||||
var requirements = new List<List<string>>();
|
||||
|
||||
if (protocolVersion < MC_1_20_2_Version)
|
||||
{
|
||||
// Builder-based (pre-1.20.2): criteria names list, then requirements
|
||||
int criteriaCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int c = 0; c < criteriaCount; c++)
|
||||
dataTypes.ReadNextString(packetData); // criterion name only, no trigger data
|
||||
}
|
||||
|
||||
// Requirements (all versions)
|
||||
int reqGroupCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int g = 0; g < reqGroupCount; g++)
|
||||
{
|
||||
int groupSize = dataTypes.ReadNextVarInt(packetData);
|
||||
var group = new List<string>(groupSize);
|
||||
for (int s = 0; s < groupSize; s++)
|
||||
group.Add(dataTypes.ReadNextString(packetData));
|
||||
requirements.Add(group);
|
||||
}
|
||||
|
||||
// sendsTelemetryEvent (added in 1.20, present in all versions since)
|
||||
if (protocolVersion >= MC_1_20_Version)
|
||||
dataTypes.ReadNextBool(packetData);
|
||||
|
||||
addedDefinitions[id] = (title, description, type, isHidden, requirements);
|
||||
}
|
||||
|
||||
// --- Removed advancement IDs ---
|
||||
int removedCount = dataTypes.ReadNextVarInt(packetData);
|
||||
var removedIds = new List<string>(removedCount);
|
||||
for (int i = 0; i < removedCount; i++)
|
||||
removedIds.Add(dataTypes.ReadNextString(packetData));
|
||||
|
||||
// --- Progress updates ---
|
||||
int progressCount = dataTypes.ReadNextVarInt(packetData);
|
||||
var progressMap = new Dictionary<string, Dictionary<string, bool>>(progressCount);
|
||||
|
||||
for (int i = 0; i < progressCount; i++)
|
||||
{
|
||||
string id = dataTypes.ReadNextString(packetData);
|
||||
int criteriaEntries = dataTypes.ReadNextVarInt(packetData);
|
||||
var criteria = new Dictionary<string, bool>(criteriaEntries);
|
||||
|
||||
for (int c = 0; c < criteriaEntries; c++)
|
||||
{
|
||||
string criterionName = dataTypes.ReadNextString(packetData);
|
||||
bool isDone = dataTypes.ReadNextBool(packetData);
|
||||
if (isDone)
|
||||
dataTypes.ReadNextLong(packetData); // epochMs - read and discard
|
||||
criteria[criterionName] = isDone;
|
||||
}
|
||||
|
||||
progressMap[id] = criteria;
|
||||
}
|
||||
|
||||
// showAdvancements boolean added in 1.21.11+
|
||||
if (protocolVersion >= MC_1_21_11_Version)
|
||||
dataTypes.ReadNextBool(packetData); // showAdvancements - read and discard
|
||||
|
||||
// Build Achievement records from definitions + progress
|
||||
foreach (var (id, def) in addedDefinitions)
|
||||
{
|
||||
progressMap.TryGetValue(id, out var criteria);
|
||||
criteria ??= new Dictionary<string, bool>();
|
||||
|
||||
bool isCompleted = ComputeAdvancementCompleted(def.requirements, criteria);
|
||||
|
||||
var readOnlyReqs = def.requirements.ConvertAll<IReadOnlyList<string>>(static g => g.AsReadOnly());
|
||||
added.Add(new Achievement(id, def.title, def.description, def.type, def.isHidden, isCompleted, readOnlyReqs.AsReadOnly(), criteria));
|
||||
}
|
||||
|
||||
// Also build Achievement records for progress-only updates (no definition change)
|
||||
foreach (var (id, criteria) in progressMap)
|
||||
{
|
||||
if (!addedDefinitions.ContainsKey(id))
|
||||
added.Add(new Achievement(id, null, null, AchievementType.Task, false, false, [], criteria));
|
||||
}
|
||||
|
||||
handler.OnAchievementsUpdate(added, removedIds, reset);
|
||||
}
|
||||
|
||||
private static Achievement CreateLegacyAchievement(string id, bool isCompleted)
|
||||
{
|
||||
Dictionary<string, bool> criteria = new(StringComparer.Ordinal)
|
||||
{
|
||||
[id] = isCompleted
|
||||
};
|
||||
IReadOnlyList<string>[] requirements = [[id]];
|
||||
return new Achievement(id, null, null, AchievementType.Legacy, false, isCompleted, requirements, criteria);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute whether an advancement is completed based on AND-of-ORs requirements.
|
||||
/// </summary>
|
||||
private static bool ComputeAdvancementCompleted(List<List<string>> requirements, Dictionary<string, bool> criteria)
|
||||
{
|
||||
// Zero requirements = automatically done
|
||||
if (requirements.Count == 0)
|
||||
return true;
|
||||
|
||||
// Each OR-group must have at least one satisfied criterion
|
||||
foreach (var group in requirements)
|
||||
{
|
||||
bool groupSatisfied = false;
|
||||
foreach (string criterion in group)
|
||||
{
|
||||
if (criteria.TryGetValue(criterion, out bool done) && done)
|
||||
{
|
||||
groupSatisfied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupSatisfied)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle the SelectAdvancementTab packet.
|
||||
/// </summary>
|
||||
private void HandleSelectAdvancementTab(Queue<byte> packetData)
|
||||
{
|
||||
bool hasTab = dataTypes.ReadNextBool(packetData);
|
||||
string? tabId = hasTab ? dataTypes.ReadNextString(packetData) : null;
|
||||
handler.OnSelectAdvancementTab(tabId);
|
||||
}
|
||||
|
||||
private void HandleUnlockRecipes(Queue<byte> packetData)
|
||||
{
|
||||
int action = dataTypes.ReadNextVarInt(packetData);
|
||||
|
|
@ -3290,6 +3517,29 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
private string ReadSlotDisplayLabel(Queue<byte> packetData)
|
||||
{
|
||||
int slotDisplayType = dataTypes.ReadNextVarInt(packetData);
|
||||
|
||||
// 26.1 changed the slot display registry order, inserting 3 new types:
|
||||
// Pre-26.1: 0=empty, 1=any_fuel, 2=item, 3=item_stack, 4=tag, 5=smithing_trim, 6=with_remainder, 7=composite
|
||||
// 26.1+: 0=empty, 1=any_fuel, 2=with_any_potion, 3=only_with_component, 4=item, 5=item_stack, 6=tag, 7=dyed, 8=smithing_trim, 9=with_remainder, 10=composite
|
||||
if (protocolVersion >= MC_26_1_Version)
|
||||
{
|
||||
return slotDisplayType switch
|
||||
{
|
||||
0 => "Empty",
|
||||
1 => "Any Fuel",
|
||||
2 => ReadWithAnyPotionSlotDisplayLabel(packetData),
|
||||
3 => ReadOnlyWithComponentSlotDisplayLabel(packetData),
|
||||
4 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))),
|
||||
5 => dataTypes.ReadNextItemSlot(packetData, itemPalette)?.GetTypeString() ?? "Empty",
|
||||
6 => "#" + dataTypes.ReadNextString(packetData),
|
||||
7 => ReadDyedSlotDisplayLabel(packetData),
|
||||
8 => ReadSmithingTrimSlotDisplayLabel(packetData),
|
||||
9 => ReadWithRemainderSlotDisplayLabel(packetData),
|
||||
10 => ReadCompositeSlotDisplayLabel(packetData),
|
||||
_ => $"slot_display_{slotDisplayType}",
|
||||
};
|
||||
}
|
||||
|
||||
return slotDisplayType switch
|
||||
{
|
||||
0 => "Empty",
|
||||
|
|
@ -3304,6 +3554,34 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a with_any_potion slot display (26.1+): contains a nested SlotDisplay.
|
||||
/// </summary>
|
||||
private string ReadWithAnyPotionSlotDisplayLabel(Queue<byte> packetData)
|
||||
{
|
||||
return ReadSlotDisplayLabel(packetData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an only_with_component slot display (26.1+): contains a nested SlotDisplay and a DataComponentType VarInt ID.
|
||||
/// </summary>
|
||||
private string ReadOnlyWithComponentSlotDisplayLabel(Queue<byte> packetData)
|
||||
{
|
||||
string sourceLabel = ReadSlotDisplayLabel(packetData);
|
||||
_ = dataTypes.ReadNextVarInt(packetData); // DataComponentType registry id
|
||||
return sourceLabel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a dyed slot display (26.1+): contains two nested SlotDisplays (dye + target).
|
||||
/// </summary>
|
||||
private string ReadDyedSlotDisplayLabel(Queue<byte> packetData)
|
||||
{
|
||||
_ = ReadSlotDisplayLabel(packetData); // dye
|
||||
string targetLabel = ReadSlotDisplayLabel(packetData); // target
|
||||
return targetLabel;
|
||||
}
|
||||
|
||||
private string ReadSmithingTrimSlotDisplayLabel(Queue<byte> packetData)
|
||||
{
|
||||
string baseLabel = ReadSlotDisplayLabel(packetData);
|
||||
|
|
|
|||
|
|
@ -530,6 +530,20 @@ namespace MinecraftClient.Protocol
|
|||
/// <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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -566,6 +566,9 @@ 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>
|
||||
|
|
|
|||
|
|
@ -2269,6 +2269,18 @@ 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);
|
||||
|
|
@ -7174,5 +7186,104 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("cmd.minimap.position_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -830,6 +830,12 @@ 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>
|
||||
|
|
@ -2527,4 +2533,37 @@ see item details.</value>
|
|||
<data name="cmd.minimap.position_set" xml:space="preserve">
|
||||
<value>Minimap position 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>
|
||||
|
|
|
|||
|
|
@ -514,6 +514,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. */
|
||||
|
|
@ -1121,6 +1129,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>
|
||||
|
|
|
|||
|
|
@ -1210,6 +1210,9 @@ 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;
|
||||
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ namespace MinecraftClient.Tui
|
|||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
Margin = new Thickness(1, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
AddMotd(infoPanel, info);
|
||||
|
|
@ -47,7 +48,7 @@ namespace MinecraftClient.Tui
|
|||
Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)),
|
||||
Padding = new Thickness(1, 0),
|
||||
Child = contentPanel,
|
||||
Margin = new Thickness(0, 1),
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -180,114 +181,8 @@ namespace MinecraftClient.Tui
|
|||
private static Run Value(string text, IBrush color) =>
|
||||
new(text) { Foreground = color };
|
||||
|
||||
#region Favicon Rendering
|
||||
|
||||
private static Grid BuildFaviconGrid(string base64Png, int displaySize)
|
||||
{
|
||||
byte[] pngBytes;
|
||||
try
|
||||
{
|
||||
pngBytes = Convert.FromBase64String(base64Png);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Grid();
|
||||
}
|
||||
|
||||
int srcWidth, srcHeight;
|
||||
byte[] rgba;
|
||||
try
|
||||
{
|
||||
(srcWidth, srcHeight, rgba) = DecodePngToRgba(pngBytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Grid();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static (int Width, int Height, byte[] Rgba) DecodePngToRgba(byte[] png)
|
||||
{
|
||||
using var image = new ImageMagick.MagickImage(png);
|
||||
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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
private static Grid BuildFaviconGrid(string base64Png, int displaySize) =>
|
||||
IconGridBuilder.BuildFromBase64(base64Png, displaySize);
|
||||
|
||||
private static class McColors
|
||||
{
|
||||
|
|
|
|||
|
|
@ -229,6 +229,58 @@ 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}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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).
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -37,6 +39,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 +62,6 @@ wait_for_file_pattern() {
|
|||
return 1
|
||||
}
|
||||
|
||||
wait_for_server_ready() {
|
||||
local timeout="${1:-60}"
|
||||
local elapsed=0
|
||||
|
||||
while (( elapsed < timeout )); do
|
||||
if mc-log "$SERVER_DIR" 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
|
||||
}
|
||||
|
||||
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,7 +72,7 @@ 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
|
||||
|
|
@ -105,24 +81,7 @@ cleanup() {
|
|||
trap cleanup EXIT
|
||||
|
||||
prepare_config() {
|
||||
cp "$REPO_ROOT/MinecraftClient.ini" "$CFG"
|
||||
|
||||
sed -i \
|
||||
-e 's/Account = { Login = "test", Password = "-" }/Account = { Login = "CursorBot", Password = "-" }/' \
|
||||
-e "s/MinecraftVersion = \"auto\"/MinecraftVersion = \"$MC_VERSION\"/" \
|
||||
-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"
|
||||
bash "$REPO_ROOT/.skills/mcc-integration-testing/scripts/prepare_offline_mcc_config.sh" "$CFG" "$MC_VERSION" CursorBot >/dev/null
|
||||
}
|
||||
|
||||
send_mcc_command() {
|
||||
|
|
@ -195,23 +154,30 @@ modern_mob_and_effects() {
|
|||
run_server_command "effect give CursorBot minecraft:regeneration 10 1 true"
|
||||
}
|
||||
|
||||
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
|
||||
prepare_config
|
||||
kill_other_servers
|
||||
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
|
||||
|
||||
: > "$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