diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 144e9819..40a22587 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -8,7 +8,7 @@ on: env: PROJECT: "MinecraftClient" - target-version: "net8.0" + target-version: "net10.0" compile-flags: "--self-contained=true -c Release -p:UseAppHost=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded" jobs: @@ -45,6 +45,11 @@ jobs: run: | echo project-path=${{ github.workspace }}/${{ env.PROJECT }} >> $GITHUB_ENV echo file-ext=${{ (startsWith(matrix.target, 'win') && '.exe') || ' ' }} >> $GITHUB_ENV + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x - name: Setup Environment Variables run: | diff --git a/.skills/mcc-integration-testing/SKILL.md b/.skills/mcc-integration-testing/SKILL.md new file mode 100644 index 00000000..f6fe207f --- /dev/null +++ b/.skills/mcc-integration-testing/SKILL.md @@ -0,0 +1,56 @@ +--- +name: mcc-integration-testing +description: Repeatable local offline integration testing for Minecraft Console Client against a local Minecraft Java server. Use this whenever the user wants to validate MCC end-to-end against a local server, switch the server to persistent offline mode, run chat or server commands through FileInputBot, exercise inventory/entity handling, or perform deeper smoke testing with mobs, particles, sounds, TNT, and operator actions. +--- + +# MCC Integration Testing + +Use this skill for local MCC validation against the user's `mc-*` and `mcc-*` shell helpers. + +## Workflow + +1. Source `~/.zshrc` in command invocations. +2. Do not read `~/.zshrc` directly. +3. Ensure the target server is configured for persistent offline testing: + - `online-mode=false` + - `enforce-secure-profile=false` + - `enable-rcon=true` + - `rcon.password=test123` +4. Build with `mcc-build`. +5. Run the scripted scenario with `scripts/run_full_spectrum_test.sh`. +6. Summarize the evidence with `scripts/summarize_test_run.sh`. + +## Required Preconditions + +- Server jar exists under `~/Minecraft/Servers//server.jar` +- `eula.txt` contains `eula=true` +- Repo root `MinecraftClient.ini` is the offline MCC test profile +- The MCC config round-trip issue must be fixed before relying on repeated launches + +## Scripts + +- `scripts/ensure_offline_server.sh` + - Generates `server.properties` if missing + - Applies persistent offline and RCON settings +- `scripts/run_full_spectrum_test.sh` + - Builds MCC + - Starts server and MCC + - Runs the full-spectrum scenario + - Verifies key MCC and server log assertions +- `scripts/summarize_test_run.sh` + - Prints the most relevant evidence from the latest run directory + +## Scenario Coverage + +The scripted run should cover: + +- offline join +- MCC-originated chat +- MCC-originated slash command +- internal MCC commands such as `health`, `list`, `inventory`, and `entity` +- OP + creative mode +- passive and hostile mob spawns +- representative sound and particle events +- TNT / explosion handling + +If a command syntax needs to be checked, use `references/command-matrix.md`. diff --git a/.skills/mcc-integration-testing/evals/evals.json b/.skills/mcc-integration-testing/evals/evals.json new file mode 100644 index 00000000..1eb5a870 --- /dev/null +++ b/.skills/mcc-integration-testing/evals/evals.json @@ -0,0 +1,39 @@ +{ + "skill_name": "mcc-integration-testing", + "evals": [ + { + "id": 1, + "prompt": "Configure the local 1.21.11 MCC test server for persistent offline mode, then run a deep MCC integration test covering chat, operator actions, creative inventory, entity tracking, sounds, particles, and TNT.", + "expected_output": "The server is left in offline mode with RCON enabled, MCC joins successfully, and the run reports clear pass or fail evidence from both MCC and server logs.", + "files": [], + "expectations": [ + "The workflow sources ~/.zshrc without reading it directly.", + "The server is configured with online-mode=false and enable-rcon=true.", + "The run uses both mc-rcon and mcc-cmd.", + "The result includes evidence from MCC output and server logs." + ] + }, + { + "id": 2, + "prompt": "Validate that MCC still works after a runtime or framework change by performing a repeatable offline smoke test against the local vanilla server and exercising entity and inventory handling.", + "expected_output": "The response runs the repeatable local workflow, checks for a successful join, verifies inventory and entity commands, and calls out any configuration or protocol regression.", + "files": [], + "expectations": [ + "The workflow builds MCC before running the server scenario.", + "The workflow checks inventory and entity handling explicitly.", + "The response flags config reload failures as regressions." + ] + }, + { + "id": 3, + "prompt": "Use the local MCC testing workflow to run a full-spectrum client/server exercise and summarize the important evidence only.", + "expected_output": "The response runs the scripted scenario and returns a concise summary with pass/fail status, affected commands, and log evidence.", + "files": [], + "expectations": [ + "The workflow uses the scripted test runner.", + "The summary includes join status, MCC command coverage, and server-side effects.", + "The summary points to the saved log locations." + ] + } + ] +} diff --git a/.skills/mcc-integration-testing/references/command-matrix.md b/.skills/mcc-integration-testing/references/command-matrix.md new file mode 100644 index 00000000..9880f2f8 --- /dev/null +++ b/.skills/mcc-integration-testing/references/command-matrix.md @@ -0,0 +1,53 @@ +# Command Matrix + +This skill uses a fixed set of stable commands for local offline integration testing. + +## MCC-side commands via `mcc-cmd` + +- `health` +- `list` +- `inventory player list` +- `/gamemode creative` +- `inventory creativegive 36 Diamond 16` +- `entity` +- `/time query daytime` +- `smoke_test_from_mcc_full_spectrum` + +Notes: +- Lines starting with `/` are sent to the server as chat/commands. +- Non-slash lines are treated as MCC internal commands first, then fall back to chat. + +## Server-side commands via `mc-rcon` + +- `op CursorBot` +- `gamerule sendCommandFeedback true` +- `gamerule logAdminCommands true` +- `time set day` +- `weather clear` + +## Representative entity coverage + +- `execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~` +- `execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~` +- `execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~` +- `execute as CursorBot at @s run summon minecraft:skeleton ~8 ~ ~` +- `execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~` +- `execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~` +- `execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2` + +## Representative particle coverage + +- `execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force` +- `execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force` +- `execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force` +- `execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force` + +## Representative sound coverage + +- `execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0` +- `execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0` + +## Explosion coverage + +- `execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~` +- `execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~` diff --git a/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh new file mode 100755 index 00000000..2aab4267 --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/ensure_offline_server.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env zsh +set -euo pipefail + +set +eu +source ~/.zshrc +set -eu + +VERSION="${1:-1.21.11-Vanilla}" +SERVER_DIR="${MCC_SERVERS:?}/$VERSION" +PROPS_FILE="$SERVER_DIR/server.properties" +SESSION_NAME="mc-${VERSION//./_}" + +if [[ ! -d "$SERVER_DIR" ]]; then + echo "Server directory not found: $SERVER_DIR" >&2 + exit 1 +fi + +if [[ ! -f "$SERVER_DIR/eula.txt" ]] || ! grep -Eq '^eula=true$' "$SERVER_DIR/eula.txt"; then + echo "Missing accepted EULA in $SERVER_DIR/eula.txt" >&2 + exit 1 +fi + +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" + else + printf '%s=%s\n' "$key" "$value" >> "$PROPS_FILE" + fi +} + +if [[ ! -f "$PROPS_FILE" ]]; then + mc-start "$VERSION" + wait_for_server_ready + mc-stop "$VERSION" + wait_for_server_stop +fi + +if server_running; then + mc-stop "$VERSION" + wait_for_server_stop +fi + +upsert_property "online-mode" "false" +upsert_property "enforce-secure-profile" "false" +upsert_property "enable-rcon" "true" +upsert_property "rcon.port" "25575" +upsert_property "rcon.password" "test123" + +echo "Configured $VERSION for persistent offline testing" diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh new file mode 100755 index 00000000..6648771a --- /dev/null +++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env zsh +set -euo pipefail + +set +eu +source ~/.zshrc +set -eu + +SCRIPT_DIR="${0:A:h}" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +VERSION="${1:-1.21.11-Vanilla}" +RUN_ROOT="${TMPDIR:-/tmp}/mcc-integration-testing" +RUN_ID="$(date +%Y%m%d-%H%M%S)" +RUN_DIR="$RUN_ROOT/$RUN_ID" +SERVER_LOG_FILE="$MCC_SERVERS/$VERSION/logs/latest.log" +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" +INPUT_FILE="$REPO_ROOT/mcc_input.txt" +MCC_PID="" + +mkdir -p "$RUN_DIR" + +cleanup() { + if [[ -n "${MCC_PID:-}" ]] && kill -0 "$MCC_PID" 2>/dev/null; then + mcc-cmd "quit" >/dev/null 2>&1 || true + sleep 2 + kill "$MCC_PID" 2>/dev/null || true + wait "$MCC_PID" 2>/dev/null || true + fi + + mc-stop "$VERSION" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +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 +} + +wait_for_server_log_pattern() { + local pattern="$1" + local description="$2" + local timeout="${3:-60}" + local elapsed=0 + + while (( elapsed < timeout )); do + if [[ -f "$SERVER_LOG_FILE" ]] && grep -Fq "$pattern" "$SERVER_LOG_FILE"; then + return 0 + fi + sleep 1 + ((elapsed += 1)) + done + + echo "Timed out waiting for server log: $description" >&2 + return 1 +} + +capture_server_logs() { + mc-log "$VERSION" 400 > "$SERVER_TMUX_LOG" 2>/dev/null || true + if [[ -f "$SERVER_LOG_FILE" ]]; then + cp "$SERVER_LOG_FILE" "$SERVER_FILE_LOG" + fi +} + +fail() { + capture_server_logs + echo "FAIL: $1" >&2 + echo "Run directory: $RUN_DIR" >&2 + exit 1 +} + +assert_contains() { + local file="$1" + local pattern="$2" + local description="$3" + + grep -Fq "$pattern" "$file" || fail "$description" +} + +assert_not_contains() { + local file="$1" + local pattern="$2" + local description="$3" + + if grep -Fq "$pattern" "$file"; then + fail "$description" + fi +} + +run_server_command() { + local cmd="$1" + echo "SERVER> $cmd" + mc-rcon "$cmd" >/dev/null || fail "Server command failed: $cmd" +} + +run_mcc_command() { + local cmd="$1" + echo "MCC> $cmd" + mcc-cmd "$cmd" + sleep 2 +} + +"$SCRIPT_DIR/ensure_offline_server.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" + +echo "Starting MCC..." +mcc-run 25565 > "$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_server_log_pattern "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" +run_server_command "gamerule logAdminCommands true" +run_server_command "time set day" +run_server_command "weather clear" +sleep 2 + +run_mcc_command "health" +run_mcc_command "list" +run_mcc_command "inventory player list" +run_mcc_command "/gamemode creative" +run_mcc_command "inventory creativegive 36 Diamond 16" +run_mcc_command "inventory player list" +run_mcc_command "entity" +run_mcc_command "/time query daytime" +run_mcc_command "smoke_test_from_mcc_full_spectrum" + +run_server_command "execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~" +run_server_command "execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~" +run_server_command "execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~" +run_server_command "execute as CursorBot at @s run summon minecraft:skeleton ~8 ~ ~" +run_server_command "execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~" +run_server_command "execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~" +run_server_command "execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2" + +sleep 2 +run_mcc_command "entity" + +run_server_command "execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force" +run_server_command "execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force" +run_server_command "execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force" +run_server_command "execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force" + +run_server_command "execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0" +run_server_command "execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0" + +run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~" +sleep 2 +run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~" + +sleep 6 +capture_server_logs + +assert_contains "$MCC_LOG" "Server was successfully joined." "MCC never joined the server" +assert_contains "$MCC_LOG" "[FileInput] > inventory player list" "Inventory command was not executed" +assert_contains "$MCC_LOG" "[FileInput] > entity" "Entity command was not executed" +assert_contains "$MCC_LOG" "[FileInput] > /gamemode creative" "Creative mode command was not executed from MCC" +assert_contains "$MCC_LOG" "Requested Diamond x16 in slot #36" "Creative inventory give did not succeed" +assert_contains "$MCC_LOG" "smoke_test_from_mcc_full_spectrum" "Client-originated chat was not observed" +assert_not_contains "$MCC_LOG" "Please enable InventoryHandling" "Inventory handling is still disabled" +assert_not_contains "$MCC_LOG" "Please enable EntityHandling" "Entity handling is still disabled" +assert_not_contains "$MCC_LOG" "You must be in Creative gamemode" "Creative mode was not active when creativegive ran" +assert_not_contains "$MCC_LOG" "Failed to load settings" "MCC failed to reload its config" + +assert_contains "$SERVER_FILE_LOG" "CursorBot joined the game" "Server never saw CursorBot join" +assert_contains "$SERVER_FILE_LOG" "smoke_test_from_mcc_full_spectrum" "Server never received the client chat message" +assert_contains "$SERVER_FILE_LOG" "Displaying particle minecraft:happy_villager" "Particle events were not recorded on the server" +assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to CursorBot" "Sound events were not recorded on the server" +assert_contains "$SERVER_FILE_LOG" "Summoned new Primed TNT" "TNT summon did not occur on the server" +assert_not_contains "$SERVER_FILE_LOG" "Sending unknown packet 'clientbound/minecraft:disconnect'" "Server hit the disconnect packet regression during the test" + +cat <&2 + exit 1 +fi + +MCC_LOG="$RUN_DIR/mcc.log" +SERVER_LOG="$RUN_DIR/server-latest.log" +BUILD_LOG="$RUN_DIR/build.log" + +echo "Run directory: $RUN_DIR" +echo +echo "Build result:" +grep -E "Warning\(s\)|Error\(s\)|Time Elapsed" "$BUILD_LOG" || true +echo +echo "MCC highlights:" +grep -E "Server was successfully joined|FileInput|smoke_test_from_mcc_full_spectrum|There are [0-9]+ of a max|health|Creative" "$MCC_LOG" || true +echo +echo "Server highlights:" +grep -E "joined the game|Made CursorBot a server operator|game mode|smoke_test_from_mcc_full_spectrum|summon|particle|playsound|tnt" "$SERVER_LOG" || true diff --git a/MinecraftClient/ChatBots/AntiAFK.cs b/MinecraftClient/ChatBots/AntiAFK.cs index 30d47189..055416a0 100644 --- a/MinecraftClient/ChatBots/AntiAFK.cs +++ b/MinecraftClient/ChatBots/AntiAFK.cs @@ -64,6 +64,12 @@ namespace MinecraftClient.ChatBots { public double min, max; + public Range() + { + min = 0; + max = 0; + } + public Range(int value) { min = max = value; @@ -180,4 +186,4 @@ namespace MinecraftClient.ChatBots currentLocation.Z + random.Next(range * -1, range)); } } -} \ No newline at end of file +} diff --git a/MinecraftClient/ChatBots/AutoCraft.cs b/MinecraftClient/ChatBots/AutoCraft.cs index 52d691ab..861bc2be 100644 --- a/MinecraftClient/ChatBots/AutoCraft.cs +++ b/MinecraftClient/ChatBots/AutoCraft.cs @@ -106,6 +106,13 @@ namespace MinecraftClient.ChatBots { public double X, Y, Z; + public LocationConfig() + { + X = 0; + Y = 0; + Z = 0; + } + public LocationConfig(double X, double Y, double Z) { this.X = X; diff --git a/MinecraftClient/ChatBots/AutoDig.cs b/MinecraftClient/ChatBots/AutoDig.cs index 17c27895..e1da7a51 100644 --- a/MinecraftClient/ChatBots/AutoDig.cs +++ b/MinecraftClient/ChatBots/AutoDig.cs @@ -85,6 +85,13 @@ namespace MinecraftClient.ChatBots { public double x, y, z; + public Coordination() + { + x = 0; + y = 0; + z = 0; + } + public Coordination(double x, double y, double z) { this.x = x; this.y = y; this.z = z; diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 09ee140f..f2ab906d 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -103,6 +103,12 @@ namespace MinecraftClient.ChatBots public Coordination? XYZ; public Facing? facing; + public LocationConfig() + { + XYZ = null; + facing = null; + } + public LocationConfig(double yaw, double pitch) { this.XYZ = null; @@ -125,6 +131,13 @@ namespace MinecraftClient.ChatBots { public double x, y, z; + public Coordination() + { + x = 0; + y = 0; + z = 0; + } + public Coordination(double x, double y, double z) { this.x = x; this.y = y; this.z = z; @@ -135,6 +148,12 @@ namespace MinecraftClient.ChatBots { public double yaw, pitch; + public Facing() + { + yaw = 0; + pitch = 0; + } + public Facing(double yaw, double pitch) { this.yaw = yaw; this.pitch = pitch; diff --git a/MinecraftClient/ChatBots/AutoRelog.cs b/MinecraftClient/ChatBots/AutoRelog.cs index 24e6570a..06dd4cb1 100644 --- a/MinecraftClient/ChatBots/AutoRelog.cs +++ b/MinecraftClient/ChatBots/AutoRelog.cs @@ -57,6 +57,12 @@ namespace MinecraftClient.ChatBots { public double min, max; + public Range() + { + min = 0; + max = 0; + } + public Range(int value) { min = max = value; diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index b46ad770..6f3ab5c4 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -284,7 +284,7 @@ namespace MinecraftClient.ChatBots if (text != null) messageBuilder.WithContent(text); - messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); + messageBuilder.AddFiles(new Dictionary() { { filePath, fs } }); discordBotClient!.SendMessageAsync(discordChannel, messageBuilder).Wait(Config.Message_Send_Timeout * 1000); } @@ -301,7 +301,7 @@ namespace MinecraftClient.ChatBots if (!CanSendMessages()) return; - SendMessage(new DiscordMessageBuilder().WithFile(fileStream)); + SendMessage(new DiscordMessageBuilder().AddFile(fileStream)); } private bool CanSendMessages() diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs index c0c3aa8e..6a9f6bc4 100644 --- a/MinecraftClient/ChatBots/Map.cs +++ b/MinecraftClient/ChatBots/Map.cs @@ -259,7 +259,8 @@ namespace MinecraftClient.ChatBots { using (var image = new MagickImage(fileName)) { - var size = new MagickGeometry(Config.Resize_To, Config.Resize_To); + uint resizeTo = (uint)Math.Max(Config.Resize_To, 1); + var size = new MagickGeometry(resizeTo, resizeTo); size.IgnoreAspectRatio = true; image.Resize(size); diff --git a/MinecraftClient/ChatBots/ScriptScheduler.cs b/MinecraftClient/ChatBots/ScriptScheduler.cs index 0e15949d..9db48c09 100644 --- a/MinecraftClient/ChatBots/ScriptScheduler.cs +++ b/MinecraftClient/ChatBots/ScriptScheduler.cs @@ -116,6 +116,12 @@ namespace MinecraftClient.ChatBots public bool Enable = false; public TimeSpan[] Times; + public TriggerOnTimeConfig() + { + Enable = false; + Times = Array.Empty(); + } + public TriggerOnTimeConfig(bool Enable, TimeSpan[] Time) { this.Enable = Enable; @@ -134,6 +140,13 @@ namespace MinecraftClient.ChatBots public bool Enable = false; public double MinTime, MaxTime; + public TriggerOnIntervalConfig() + { + Enable = false; + MinTime = 0; + MaxTime = 0; + } + public TriggerOnIntervalConfig(double value) { this.Enable = true; diff --git a/MinecraftClient/ChatBots/TelegramBridge.cs b/MinecraftClient/ChatBots/TelegramBridge.cs index f30b7107..1140756a 100644 --- a/MinecraftClient/ChatBots/TelegramBridge.cs +++ b/MinecraftClient/ChatBots/TelegramBridge.cs @@ -12,7 +12,6 @@ using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -using Telegram.Bot.Types.InputFiles; using Tomlet.Attributes; using File = System.IO.File; @@ -205,7 +204,7 @@ namespace MinecraftClient.ChatBots try { - botClient!.SendTextMessageAsync(Config.ChannelId.Trim(), message, ParseMode.Markdown).Wait(Config.Message_Send_Timeout); + botClient!.SendMessage(Config.ChannelId.Trim(), message, parseMode: ParseMode.Markdown).Wait(Config.Message_Send_Timeout); } catch (Exception e) { @@ -224,9 +223,9 @@ namespace MinecraftClient.ChatBots string fileName = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; Stream stream = File.OpenRead(filePath); - botClient!.SendDocumentAsync( + botClient!.SendDocument( Config.ChannelId.Trim(), - document: new InputOnlineFile(content: stream, fileName), + document: InputFile.FromStream(stream, fileName), caption: text, parseMode: ParseMode.Markdown).Wait(Config.Message_Send_Timeout * 1000); } @@ -260,14 +259,14 @@ namespace MinecraftClient.ChatBots cancellationToken = new CancellationTokenSource(); botClient.StartReceiving( - updateHandler: HandleUpdateAsync, - pollingErrorHandler: HandlePollingErrorAsync, - receiverOptions: new ReceiverOptions + HandleUpdateAsync, + HandlePollingErrorAsync, + new ReceiverOptions { // receive all update types AllowedUpdates = Array.Empty() }, - cancellationToken: cancellationToken.Token + cancellationToken.Token ); IsConnected = true; @@ -313,9 +312,9 @@ namespace MinecraftClient.ChatBots if (text.ToLower().Contains(".chatid")) { - await botClient.SendTextMessageAsync(chatId: chatId, - replyToMessageId: message.MessageId, + await botClient.SendMessage(chatId: chatId, text: $"Chat ID: {chatId}", + replyParameters: message.MessageId, cancellationToken: _cancellationToken, parseMode: ParseMode.Markdown); return; @@ -324,10 +323,10 @@ namespace MinecraftClient.ChatBots if (Config.Authorized_Chat_Ids.Length > 0 && !Config.Authorized_Chat_Ids.Contains(chatId)) { LogDebugToConsole($"Unauthorized message '{messageText}' received in a chat with with an ID: {chatId} !"); - await botClient.SendTextMessageAsync( + await botClient.SendMessage( chatId: chatId, - replyToMessageId: message.MessageId, text: Translations.bot_TelegramBridge_unauthorized, + replyParameters: message.MessageId, cancellationToken: _cancellationToken, parseMode: ParseMode.Markdown); return; @@ -347,10 +346,10 @@ namespace MinecraftClient.ChatBots if (command.ToLower().Contains("quit") || command.ToLower().Contains("exit")) { - await botClient.SendTextMessageAsync( + await botClient.SendMessage( chatId: chatId, - replyToMessageId: message.MessageId, text: $"{Translations.bot_TelegramBridge_quit_disabled}", + replyParameters: message.MessageId, cancellationToken: _cancellationToken, parseMode: ParseMode.Markdown); return;; @@ -359,11 +358,10 @@ namespace MinecraftClient.ChatBots CmdResult result = new(); PerformInternalCommand(command, ref result); - await botClient.SendTextMessageAsync( + await botClient.SendMessage( chatId: chatId, - replyToMessageId: - message.MessageId, text: $"{Translations.bot_TelegramBridge_command_executed}:\n\n{result}", + replyParameters: message.MessageId, cancellationToken: _cancellationToken, parseMode: ParseMode.Markdown); } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index bd71f18a..68e605b1 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 Exe publish\ false @@ -28,25 +28,22 @@ - - - - + + + - - - - - - - - - + + + + + + + + NU1701 - - + diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index ae518b78..d09a8e8a 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -70,7 +70,7 @@ namespace MinecraftClient options.Dsn = SentryDSN; options.AutoSessionTracking = true; options.IsGlobalModeEnabled = true; - options.EnableTracing = true; + options.TracesSampleRate = 1.0; options.SendDefaultPii = false; }); diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index ab00dfc6..bdc1a8eb 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -350,7 +350,7 @@ namespace MinecraftClient.Protocol.Handlers { break; } - catch (Ionic.Zlib.ZlibException) + catch (System.IO.InvalidDataException) { break; } diff --git a/MinecraftClient/Protocol/Handlers/ZlibUtils.cs b/MinecraftClient/Protocol/Handlers/ZlibUtils.cs index 62f8bf85..bff4131d 100644 --- a/MinecraftClient/Protocol/Handlers/ZlibUtils.cs +++ b/MinecraftClient/Protocol/Handlers/ZlibUtils.cs @@ -1,12 +1,10 @@ -using Ionic.Zlib; +using System.IO; +using System.IO.Compression; namespace MinecraftClient.Protocol.Handlers { /// /// Quick Zlib compression handling for network packet compression. - /// Note: Underlying compression handling is taken from the DotNetZip Library. - /// This library is open source and provided under the Microsoft Public License. - /// More info about DotNetZip at dotnetzip.codeplex.com. /// public static class ZlibUtils { @@ -17,16 +15,13 @@ namespace MinecraftClient.Protocol.Handlers /// Compressed data as a byte array public static byte[] Compress(byte[] to_compress) { - byte[] data; - using (System.IO.MemoryStream memstream = new()) + using MemoryStream memstream = new(); + using (ZLibStream stream = new(memstream, CompressionMode.Compress, leaveOpen: true)) { - using (ZlibStream stream = new(memstream, CompressionMode.Compress)) - { - stream.Write(to_compress, 0, to_compress.Length); - } - data = memstream.ToArray(); + stream.Write(to_compress, 0, to_compress.Length); } - return data; + + return memstream.ToArray(); } /// @@ -37,10 +32,20 @@ namespace MinecraftClient.Protocol.Handlers /// Decompressed data as a byte array public static byte[] Decompress(byte[] to_decompress, int size_uncompressed) { - ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress); + using MemoryStream compressedStream = new(to_decompress, writable: false); + using ZLibStream stream = new(compressedStream, CompressionMode.Decompress); + byte[] packetData_decompressed = new byte[size_uncompressed]; - stream.Read(packetData_decompressed, 0, size_uncompressed); - stream.Close(); + int totalRead = 0; + while (totalRead < size_uncompressed) + { + int read = stream.Read(packetData_decompressed, totalRead, size_uncompressed - totalRead); + if (read <= 0) + break; + + totalRead += read; + } + return packetData_decompressed; } @@ -51,12 +56,14 @@ namespace MinecraftClient.Protocol.Handlers /// Decompressed data as byte array public static byte[] Decompress(byte[] to_decompress) { - ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress); + using MemoryStream compressedStream = new(to_decompress, writable: false); + using ZLibStream stream = new(compressedStream, CompressionMode.Decompress); byte[] buffer = new byte[16 * 1024]; - using System.IO.MemoryStream decompressedBuffer = new(); + using MemoryStream decompressedBuffer = new(); int read; while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) decompressedBuffer.Write(buffer, 0, read); + return decompressedBuffer.ToArray(); } } diff --git a/MinecraftClient/Protocol/ReplayHandler.cs b/MinecraftClient/Protocol/ReplayHandler.cs index be567caf..697e23c3 100644 --- a/MinecraftClient/Protocol/ReplayHandler.cs +++ b/MinecraftClient/Protocol/ReplayHandler.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; -using Ionic.Zip; using MinecraftClient.Mapping; using MinecraftClient.Protocol.Handlers; using MinecraftClient.Protocol.Handlers.PacketPalettes; @@ -138,12 +138,18 @@ namespace MinecraftClient.Protocol using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open)) { using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open); - using ZipOutputStream zs = new(Path.Combine(ReplayFileDirectory, replayFileName)); - zs.PutNextEntry(recordingTmpFileName); - recordingFile.CopyTo(zs); - zs.PutNextEntry(MetaData.MetaDataFileName); - metaDataFile.CopyTo(zs); - zs.Close(); + using FileStream replayArchiveFile = new(Path.Combine(ReplayFileDirectory, replayFileName), FileMode.Create, FileAccess.Write); + using ZipArchive replayArchive = new(replayArchiveFile, ZipArchiveMode.Create); + + ZipArchiveEntry recordingEntry = replayArchive.CreateEntry(recordingTmpFileName); + using (Stream recordingEntryStream = recordingEntry.Open()) + { + recordingFile.CopyTo(recordingEntryStream); + } + + ZipArchiveEntry metadataEntry = replayArchive.CreateEntry(MetaData.MetaDataFileName); + using Stream metadataEntryStream = metadataEntry.Open(); + metaDataFile.CopyTo(metadataEntryStream); } File.Delete(Path.Combine(temporaryCache, recordingTmpFileName)); @@ -167,18 +173,29 @@ namespace MinecraftClient.Protocol using (Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open)) { - using ZipOutputStream zs = new(replayFileName); - zs.PutNextEntry(recordingTmpFileName); - // .CopyTo() method start from stream current position - // We need to reset position in order to get full content - var lastPosition = recordStream!.BaseStream.Position; - recordStream.BaseStream.Position = 0; - recordStream.BaseStream.CopyTo(zs); - recordStream.BaseStream.Position = lastPosition; + using FileStream replayArchiveFile = new(replayFileName, FileMode.Create, FileAccess.Write); + using ZipArchive replayArchive = new(replayArchiveFile, ZipArchiveMode.Create); - zs.PutNextEntry(MetaData.MetaDataFileName); - metaDataFile.CopyTo(zs); - zs.Close(); + ZipArchiveEntry recordingEntry = replayArchive.CreateEntry(recordingTmpFileName); + using (Stream recordingEntryStream = recordingEntry.Open()) + { + // .CopyTo() method start from stream current position + // We need to reset position in order to get full content + long lastPosition = recordStream!.BaseStream.Position; + try + { + recordStream.BaseStream.Position = 0; + recordStream.BaseStream.CopyTo(recordingEntryStream); + } + finally + { + recordStream.BaseStream.Position = lastPosition; + } + } + + ZipArchiveEntry metadataEntry = replayArchive.CreateEntry(MetaData.MetaDataFileName); + using Stream metadataEntryStream = metadataEntry.Open(); + metaDataFile.CopyTo(metadataEntryStream); } WriteDebugLog("Backup replay file created."); diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs index 1e99f226..81417178 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs @@ -7,7 +7,6 @@ https://github.com/laurentkempe/DynamicRun/blob/master/LICENSE using System; using System.Collections.Generic; using System.IO; -using System.IO.MemoryMappedFiles; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -117,10 +116,11 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder File.Copy(executablePath, tempFile); // Access the contents of the executable. - ExecutableReader e = new(); - var viewAccessor = MemoryMappedFile.CreateFromFile(tempFile, FileMode.Open).CreateViewAccessor(); - var manifest = e.ReadManifest(viewAccessor); - var files = manifest.Files; + using ExecutableReader executableReader = new(tempFile); + if (!executableReader.IsSingleFile) + throw new InvalidOperationException("[Script Error] The executable is not a single-file bundle."); + + var files = executableReader.Bundle.Files; Stream? assemblyStream; @@ -133,8 +133,8 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder if (string.IsNullOrEmpty(loadedAssembly.Location)) { // Check if we can access the file from the executable. var reference = files.FirstOrDefault(x => - x.RelativePath.Remove(x.RelativePath.Length - 4) == refs.Name); - var refCount = files.Count(x => x.RelativePath.Remove(x.RelativePath.Length - 4) == refs.Name); + Path.GetFileNameWithoutExtension(x.RelativePath) == refs.Name); + var refCount = files.Count(x => Path.GetFileNameWithoutExtension(x.RelativePath) == refs.Name); if (refCount > 1) { // Safety net for the case where the assembly is referenced multiple times. // Should not happen normally, but we can make exceptions when it does happen. @@ -147,17 +147,13 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder "[Script Error] The executable does not contain a referenced assembly. Assembly name: " + refs.Name); } - assemblyStream = GetStreamForFileEntry(viewAccessor, reference); + assemblyStream = reference.AsStream(); references.Add(MetadataReference.CreateFromStream(assemblyStream!)); continue; } references.Add(MetadataReference.CreateFromFile(loadedAssembly.Location)); } - - // Cleanup. - viewAccessor.Flush(); - viewAccessor.Dispose(); } else { @@ -176,14 +172,6 @@ namespace MinecraftClient.Scripting.DynamicRun.Builder assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); } - private static Stream? GetStreamForFileEntry(MemoryMappedViewAccessor viewAccessor, FileEntry file) - { - if (typeof(BundleExtractor).GetMethod("GetStreamForFileEntry", BindingFlags.NonPublic | BindingFlags.Static)!.Invoke(null, new object[] { viewAccessor, file }) is not Stream stream) - throw new InvalidOperationException("[Script Error] The executable does not contain the assembly. Assembly name: " + file.RelativePath); - - return stream; - } - internal struct CompileResult { internal byte[]? Assembly; diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 684e5a98..8a7ca0c1 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -658,6 +658,12 @@ namespace MinecraftClient { public string Login = string.Empty, Password = string.Empty; + public AccountInfoConfig() + { + Login = string.Empty; + Password = string.Empty; + } + public AccountInfoConfig(string Login) { this.Login = Login; @@ -676,6 +682,12 @@ namespace MinecraftClient public string Host = string.Empty; public ushort? Port = null; + public ServerInfoConfig() + { + Host = string.Empty; + Port = null; + } + public ServerInfoConfig(string Host) { string[] sip = Host.Split(new[] { ":", ":" }, StringSplitOptions.None); @@ -699,6 +711,12 @@ namespace MinecraftClient public string Host = string.Empty; public int Port = 443; + public AuthlibServer() + { + Host = string.Empty; + Port = 443; + } + public AuthlibServer(string Host) { string[] sip = Host.Split(new[] { ":", ":" }, StringSplitOptions.None);