mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
TUI for MCC (#2976)
* feat: implement interactive TUI inventory viewer - Added InventoryTui command to open an interactive terminal user interface for inventory management. - Introduced InventoryApp and InventoryMainView classes for TUI layout and functionality. - Created InventoryViewModel and SlotViewModel to manage inventory data and display. - Integrated Consolonia for enhanced console UI experience. - Updated McClient to support console message handling during TUI operation. These changes enhance user interaction with inventory management in Minecraft Console Client. * feat: enhance debugging workflow with mcc-debug.sh and TUI support - Introduced mcc-debug.sh for streamlined one-step build, server start, and MCC launch. - Added TUI mode for improved user experience during debugging sessions. - Updated mcc-env.sh to include new debug helpers and TUI mode functionality. - Enhanced SKILL.md with detailed instructions for using the new debugging tools and console modes. These changes significantly improve the debugging process for Minecraft Console Client, making it more efficient and user-friendly. * feat: introduce TUI console backend and related enhancements - Added ClassicConsoleBackend and TuiConsoleBackend to support different console I/O modes. - Implemented IConsoleBackend interface for better abstraction of console operations. - Enhanced ConsoleIO to utilize the new backend structure for input and output handling. - Introduced MainTuiView and MccTuiApp for a full-screen TUI experience using Avalonia. - Updated InventoryTuiHost to manage TUI lifecycle and interactions. These changes significantly improve the console experience in Minecraft Console Client, providing a more flexible and user-friendly interface. * refactor: remove InventoryTui command implementation - Deleted the InventoryTui class, which provided an interactive terminal user interface for inventory management. - This change simplifies the command structure as part of ongoing improvements to the console experience in Minecraft Console Client. The removal of this command is aligned with recent enhancements to the console backend and user interface. * refactor: update InventoryMainView and MainTuiView for improved UI handling - Changed several fields from readonly to mutable in InventoryMainView to allow for dynamic updates. - Introduced a new McColorParser class for parsing Minecraft color codes and creating colored text blocks. - Enhanced MainTuiView to support formatted log lines and improved notification handling. - Updated chat scrolling behavior to ensure better user experience during text input and log display. These changes streamline the UI components and enhance the overall console experience in Minecraft Console Client. * feat: enhance command input handling in MainTuiView - Updated command input to use event routing for better key handling. - Added support for Ctrl key shortcuts to improve text manipulation (e.g., word deletion, caret movement). - Implemented text cleaning on input change to prevent newline characters. - Improved health and food status bar rendering with a new method for building bar text. These changes significantly enhance the user experience in the TUI by providing more intuitive command input functionality. * feat: enhance TUI command suggestion functionality - Introduced a new CommandSuggestion struct for backend-independent suggestion handling. - Updated ConsoleIO to streamline suggestion updates for both Classic and TUI backends. - Enhanced MainTuiView to display command suggestions with improved visibility and interaction. - Increased the maximum number of displayed suggestions from 6 to 10 for better user experience. These changes significantly improve the command input experience in the TUI, making it more intuitive and user-friendly. * feat: enhance offline command autocomplete functionality - Introduced an OfflineAutocompleteHandler to provide command suggestions for offline commands. - Added support for tab cycling through suggestions in the TUI. - Improved command input handling to clear suggestions when necessary and manage user input more effectively. - Updated TuiConsoleBackend to integrate with the new autocomplete feature. These changes significantly enhance the user experience by making command input more intuitive and responsive in offline scenarios. * feat: enhance localization and user feedback in TUI - Updated various TUI components to utilize localized strings for improved user experience. - Enhanced debug state output with translated labels for better clarity. - Improved inventory command help messages with localized text. - Added new translations for console mode descriptions and inventory viewer prompts. These changes significantly enhance the usability and accessibility of the TUI in Minecraft Console Client, making it more user-friendly and informative. * feat: enforce localization for user-facing strings in AGENTS.md - Added guidelines to ensure all user-facing text, including log messages and error messages, is managed through the translation system. - Specified the use of `Translations.resx` and `Translations.Designer.cs` for localization, emphasizing the importance of avoiding hardcoded strings in source code. These changes improve the consistency and accessibility of user-facing content across the application.
This commit is contained in:
parent
a17b7d4bd3
commit
8456e363f5
28 changed files with 4272 additions and 145 deletions
|
|
@ -13,7 +13,18 @@ Use this skill when the task needs a real local server loop, not just code readi
|
|||
- Runtime target: `.NET 10` / `net10.0`
|
||||
- Environment: WSL Ubuntu, Java 21, tmux, python3
|
||||
- Default server root: `${MCC_SERVERS:-$MCC_REPO/MinecraftOfficial/downloads}`
|
||||
- Default validation target when the user does not specify a version: `1.21.11-Vanilla`
|
||||
- Default validation target when the user does not specify a version: `1.21.11`
|
||||
|
||||
## Console modes
|
||||
|
||||
MCC supports two console modes selectable via `ConsoleMode` in `[Console.General]`:
|
||||
|
||||
| Mode | Backend | Best for |
|
||||
|------|---------|----------|
|
||||
| `classic` | `ClassicConsoleBackend` (ConsoleInteractive) | Normal use, legacy CI/scripts, `FileInput` mode |
|
||||
| `tui` | `TuiConsoleBackend` (Avalonia/Consolonia) | Full-screen TUI with scrollable log, command input, popup inventory |
|
||||
|
||||
Both modes support the same commands and input/output through `ConsoleIO.Backend`. The mode is determined at startup from config; `BasicIO` CLI arg overrides to simple stdio.
|
||||
|
||||
## Core rules
|
||||
|
||||
|
|
@ -35,16 +46,16 @@ Interactive shell:
|
|||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mc-start 1.21.11-Vanilla
|
||||
mc-log 1.21.11-Vanilla 100
|
||||
mc-start 1.21.11
|
||||
mc-log 1.21.11 100
|
||||
mc-rcon "op CursorBot"
|
||||
mc-stop 1.21.11-Vanilla
|
||||
mc-stop 1.21.11
|
||||
```
|
||||
|
||||
Non-interactive shell:
|
||||
|
||||
```bash
|
||||
tools/start-server.sh 1.21.11-Vanilla
|
||||
tools/start-server.sh 1.21.11
|
||||
tools/mc-rcon.sh "op CursorBot"
|
||||
```
|
||||
|
||||
|
|
@ -55,17 +66,158 @@ export MCC_SERVERS=/home/anon/Minecraft/Servers
|
|||
source tools/mcc-env.sh
|
||||
```
|
||||
|
||||
## Recommended automation recipe
|
||||
## One-step debug session (recommended)
|
||||
|
||||
Use this for reproducible local runs:
|
||||
The `tools/mcc-debug.sh` script handles build, server startup, config preparation, and MCC launch in one step:
|
||||
|
||||
1. Source `tools/mcc-env.sh`.
|
||||
2. Pick a concrete server directory, usually `1.21.11-Vanilla`.
|
||||
3. Copy `MinecraftClient.ini` to a temp location and pin the account, version, and feature gates there.
|
||||
4. Start the server and wait for `Done (` in the tmux log.
|
||||
5. Launch MCC with `MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "<temp-config>"`.
|
||||
6. Drive MCC through `mcc_input.txt`.
|
||||
7. Stop MCC and the server cleanly, then keep the temp logs.
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
|
||||
# Classic mode with FileInput (script-driven debugging):
|
||||
mcc-debug -v 1.21.11 --file-input
|
||||
|
||||
# Classic mode interactive (attach via tmux):
|
||||
mcc-debug -v 1.21.11
|
||||
|
||||
# TUI mode:
|
||||
mcc-debug -v 1.21.11 -m tui
|
||||
|
||||
# With debug messages enabled from start:
|
||||
mcc-debug -v 1.21.11 --file-input --debug-on
|
||||
|
||||
# Skip build (already built):
|
||||
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
|
||||
3. Ensures server is running (starts if not, waits for `Done (`)
|
||||
4. Launches MCC in the specified mode
|
||||
|
||||
### After launch
|
||||
|
||||
- **FileInput mode**: drive MCC via `mcc-cmd "debug state"` or `echo "debug state" >> mcc_input.txt`
|
||||
- **Interactive/TUI mode**: attach with `tmux attach -t mcc-debug`, type commands directly
|
||||
- **Logs**: `tail -f /tmp/mcc-debug/mcc-debug.log` (FileInput mode only; TUI/interactive mode outputs to tmux)
|
||||
- **Server RCON**: `mc-rcon "op CursorBot"`, `mc-rcon "gamemode creative CursorBot"`
|
||||
|
||||
## Debug commands (in-game)
|
||||
|
||||
### `/debug [on|off]`
|
||||
|
||||
Toggles debug logging. Now correctly syncs both `Settings.Config.Logging.DebugMessages` and `McClient.Log.DebugEnabled`.
|
||||
|
||||
### `/debug state`
|
||||
|
||||
Prints a one-shot summary of MCC's internal state:
|
||||
|
||||
```
|
||||
=== MCC Debug State ===
|
||||
Server: localhost:25565
|
||||
Username: CursorBot
|
||||
Protocol: 774
|
||||
GameMode: 1
|
||||
Health: 20.0
|
||||
Food: 20
|
||||
Location: 0.50, 80.00, 0.50
|
||||
TPS: 20.0
|
||||
Console: ClassicConsoleBackend (or TuiConsoleBackend)
|
||||
Features: Terrain Inventory Entity
|
||||
Debug: ON
|
||||
Bots (3): AutoFishing, FileInputBot, ScriptScheduler
|
||||
Players: 2 online
|
||||
```
|
||||
|
||||
This works in both classic and TUI modes.
|
||||
|
||||
## Classic mode debugging
|
||||
|
||||
### Agent workflow (FileInput mode)
|
||||
|
||||
For agents calling MCC commands programmatically:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mcc-debug -v 1.21.11 --file-input --no-build
|
||||
|
||||
# Send commands:
|
||||
mcc-cmd "debug state"
|
||||
mcc-cmd "inventory player list"
|
||||
mcc-cmd "entity"
|
||||
|
||||
# Check results:
|
||||
tail -20 /tmp/mcc-debug/mcc-debug.log
|
||||
|
||||
# Stop:
|
||||
mcc-cmd "quit"
|
||||
mc-stop 1.21.11
|
||||
```
|
||||
|
||||
### Interactive workflow
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mcc-debug -v 1.21.11
|
||||
|
||||
# In another terminal:
|
||||
tmux attach -t mcc-debug
|
||||
# Type commands directly in MCC console
|
||||
```
|
||||
|
||||
## TUI mode debugging
|
||||
|
||||
TUI mode runs Consolonia full-screen in a tmux session. Key differences:
|
||||
|
||||
1. **No pipe/redirect**: TUI needs a real tty. Cannot `| tee` or redirect stdout.
|
||||
2. **Log output is in-screen**: all output appears in the scrollable log area.
|
||||
3. **Keyboard shortcuts**: PageUp/PageDown scroll, ESC exits.
|
||||
4. **`/debug state`**: the primary way to inspect internal state since external log tailing is not available.
|
||||
5. **Dialog windows**: `/inventui` opens as an overlay dialog instead of a separate screen.
|
||||
|
||||
### Agent workflow for TUI mode
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mcc-debug -v 1.21.11 -m tui --no-build
|
||||
|
||||
# Cannot use mcc-cmd (no FileInput); must use tmux send-keys:
|
||||
tmux send-keys -t mcc-debug "/debug state" Enter
|
||||
|
||||
# Read TUI screen:
|
||||
tmux capture-pane -t mcc-debug -p -S -30
|
||||
|
||||
# Stop:
|
||||
tmux send-keys -t mcc-debug Escape
|
||||
```
|
||||
|
||||
**Caveat with tmux send-keys and Consolonia**: When sending text containing `/`, the Enter key may need to be sent separately:
|
||||
```bash
|
||||
tmux send-keys -t mcc-debug "/inventory player list"
|
||||
tmux send-keys -t mcc-debug Enter
|
||||
```
|
||||
|
||||
## mcc-env.sh quick reference
|
||||
|
||||
After `source tools/mcc-env.sh`:
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `mc-start VER` | Start MC server in tmux |
|
||||
| `mc-stop VER` | Graceful stop via stdin pipe |
|
||||
| `mc-log VER [N]` | Capture last N lines of server output |
|
||||
| `mc-rcon "CMD"` | Send RCON command |
|
||||
| `mc-kill VER` | Force-kill server tmux session |
|
||||
| `mc-list` | List running MC server sessions |
|
||||
| `mcc-build` | Build MCC |
|
||||
| `mcc-run [PORT]` | Run MCC classic+FileInput on port |
|
||||
| `mcc-tui [PORT]` | Run MCC TUI mode in tmux |
|
||||
| `mcc-cmd "CMD"` | Append command to mcc_input.txt |
|
||||
| `mcc-kill` | Kill MCC process and debug session |
|
||||
| `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 |
|
||||
|
||||
## Temporary config recipe
|
||||
|
||||
|
|
@ -84,26 +236,11 @@ sed -i \
|
|||
"$CFG"
|
||||
```
|
||||
|
||||
Add extra `sed -i` edits only for the scenario you are testing.
|
||||
|
||||
## Run MCC
|
||||
|
||||
Direct temp-config launch:
|
||||
|
||||
For TUI mode, also add:
|
||||
```bash
|
||||
cd "$MCC_REPO"
|
||||
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "$CFG" 2>&1
|
||||
sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG"
|
||||
```
|
||||
|
||||
Quick manual launch with the repo-root config:
|
||||
|
||||
```bash
|
||||
source tools/mcc-env.sh
|
||||
mcc-run 25565
|
||||
```
|
||||
|
||||
`mcc-run` is fine for manual smoke tests. Use a temp config for repeatable automation.
|
||||
|
||||
## Verify connection and a basic command
|
||||
|
||||
MCC output should include:
|
||||
|
|
@ -117,25 +254,39 @@ Server output should include:
|
|||
Basic command check:
|
||||
|
||||
```bash
|
||||
echo "inventory player list" >> mcc_input.txt
|
||||
mcc-cmd "inventory player list"
|
||||
```
|
||||
|
||||
## Typical debug loop
|
||||
|
||||
1. `source tools/mcc-env.sh`
|
||||
2. `dotnet build MinecraftClient.sln -c Release`
|
||||
3. start `1.21.11-Vanilla`
|
||||
4. wait for `Done (`
|
||||
5. launch MCC with a temp config
|
||||
6. confirm the join in both logs
|
||||
7. run one MCC command and one server command
|
||||
8. inspect logs
|
||||
9. stop both sides and iterate
|
||||
2. `mcc-debug -v 1.21.11 --file-input` (or `-m tui`)
|
||||
3. Confirm `Server was successfully joined` in log
|
||||
4. `mcc-cmd "debug state"` to verify MCC state
|
||||
5. Run test commands
|
||||
6. Inspect log output
|
||||
7. `mcc-cmd "quit"` and `mc-stop 1.21.11`
|
||||
8. Edit code, rebuild, repeat
|
||||
|
||||
## Debugging tips
|
||||
|
||||
- **`/debug state` is your primary diagnostic tool** in both modes. Use it first to verify connection, mode, and feature flags.
|
||||
- **`/debug on` now correctly enables debug logging** at runtime. Previous versions had a bug where `Log.DebugEnabled` was not synced.
|
||||
- Protocol mismatches usually show up as a version line such as `Server version : 1.21.11 (protocol vNNN)` before the failure.
|
||||
- If an early `mc-rcon` command fails, retry it before assuming the server setup is broken.
|
||||
- If a supposedly isolated run behaves strangely, check `tmux list-sessions` and kill stale `mc-*` sessions first.
|
||||
- Legacy `1.8` and `1.8.9` servers may need `use-native-transport=false` in `server.properties` on some Linux environments.
|
||||
- For timing-sensitive work, do not trust wall-clock intuition. Use a real server run and capture evidence from logs or test scripts.
|
||||
- **TUI mode tip**: if the terminal becomes unresponsive after a crash, run `stty sane && reset` to restore it.
|
||||
- **tmux capture trick**: `tmux capture-pane -t mcc-debug -p -S -50` captures the last 50 lines of a tmux session without attaching.
|
||||
|
||||
## Tool files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `tools/mcc-env.sh` | Shell functions for server/MCC management |
|
||||
| `tools/mcc-debug.sh` | One-step debug session launcher |
|
||||
| `tools/mcc-log-tail.sh` | Log tailing for MCC and/or server |
|
||||
| `tools/start-server.sh` | Server lifecycle in tmux |
|
||||
| `tools/mc-rcon.sh` | RCON command sender |
|
||||
| `tools/run-creative-e2e.sh` | Full creative mode end-to-end test |
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ Read `docs/guide/ai-assisted-development.md` before starting development work on
|
|||
- Prefer nullable-aware code, pattern matching, `ArgumentNullException.ThrowIfNull`, `Try*` APIs for expected failures, and `InvokeOnMainThread()` for cross-thread state changes.
|
||||
- Use modern C# 14 features.
|
||||
- Use provided skills proactively depending on the context, read their descriptions to determine when to use them.
|
||||
- All user-facing text (log messages, command output, TUI labels, notifications, help text, error messages) **must** go through the translation system: add entries to `Translations.resx` + `Translations.Designer.cs`, then reference `Translations.key_name` in code. Never hardcode user-visible strings directly in `.cs` files. Use `string.Format(Translations.key, ...)` for parameterized messages. Translation keys follow dot-delimited naming: `<module>.<scope>.<detail>` (e.g. `cmd.inventory.tui_opened`, `tui.inventory.controls`). Pure technical identifiers (class names, protocol constants, color codes) are exempt.
|
||||
|
||||
### DON'T
|
||||
- Don't update only `MCVer2ProtocolVersion()` or only one palette file when adding a new Minecraft version.
|
||||
|
|
@ -118,4 +119,5 @@ Read `docs/guide/ai-assisted-development.md` before starting development work on
|
|||
- Don't start background workers when `Update()` or delayed tasks are sufficient; if you must, stop them on unload/disconnect.
|
||||
- Don't leave movement locks, plugin channels, or dispatcher registrations behind.
|
||||
- Don't trust older docs over current code for supported versions or feature gates. When AGENTS.md, skills, and older docs disagree, prefer current code and current tool behavior, then update the stale source.
|
||||
- Don't hardcode user-facing strings (messages, labels, help text) directly in source code; always use `Translations.*` resources so the text can be localized.
|
||||
- Never use "—" ("em dash"), unless specifically being instructed to do so!
|
||||
|
|
|
|||
160
MinecraftClient/ClassicConsoleBackend.cs
Normal file
160
MinecraftClient/ClassicConsoleBackend.cs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Console backend wrapping the ConsoleInteractive library (existing behavior).
|
||||
/// </summary>
|
||||
public class ClassicConsoleBackend : IConsoleBackend
|
||||
{
|
||||
public event EventHandler<string>? MessageReceived;
|
||||
public event EventHandler<ConsoleInputBuffer>? OnInputChange;
|
||||
|
||||
public bool DisplayUserInput
|
||||
{
|
||||
get => ConsoleInteractive.ConsoleReader.DisplayUesrInput;
|
||||
set => ConsoleInteractive.ConsoleReader.DisplayUesrInput = value;
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.Init();
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLine(text);
|
||||
}
|
||||
|
||||
public void WriteLineFormatted(string text)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(text);
|
||||
}
|
||||
|
||||
public void BeginReadThread()
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived += ForwardMessage;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange += ForwardInputChange;
|
||||
ConsoleInteractive.ConsoleReader.BeginReadThread();
|
||||
}
|
||||
|
||||
public void StopReadThread()
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived -= ForwardMessage;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange -= ForwardInputChange;
|
||||
}
|
||||
|
||||
public string RequestImmediateInput()
|
||||
{
|
||||
return ConsoleInteractive.ConsoleReader.RequestImmediateInput();
|
||||
}
|
||||
|
||||
public string? ReadPassword()
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(false);
|
||||
var input = ConsoleInteractive.ConsoleReader.RequestImmediateInput();
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(true);
|
||||
return input;
|
||||
}
|
||||
|
||||
public void ClearInputBuffer()
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.ClearBuffer();
|
||||
}
|
||||
|
||||
public void SetInputVisible(bool visible)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(visible);
|
||||
}
|
||||
|
||||
public void SetBackreadBufferLimit(int limit)
|
||||
{
|
||||
ConsoleInteractive.ConsoleBuffer.SetBackreadBufferLimit(limit);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
}
|
||||
|
||||
#region Suggestion forwarding for classic mode
|
||||
|
||||
public void UpdateSuggestions(
|
||||
ConsoleInteractive.ConsoleSuggestion.Suggestion[] suggestions,
|
||||
Tuple<int, int> range)
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(suggestions, range);
|
||||
}
|
||||
|
||||
public void ClearSuggestions()
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
}
|
||||
|
||||
public void SetSuggestionColors(
|
||||
string textColor, string textBgColor,
|
||||
string hlTextColor, string hlTextBgColor,
|
||||
string tooltipColor, string hlTooltipColor,
|
||||
string arrowColor)
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.SetColors(
|
||||
textColor, textBgColor,
|
||||
hlTextColor, hlTextBgColor,
|
||||
tooltipColor, hlTooltipColor,
|
||||
arrowColor);
|
||||
}
|
||||
|
||||
public bool EnableSuggestionColor
|
||||
{
|
||||
get => ConsoleInteractive.ConsoleSuggestion.EnableColor;
|
||||
set => ConsoleInteractive.ConsoleSuggestion.EnableColor = value;
|
||||
}
|
||||
|
||||
public bool Enable24bitColor
|
||||
{
|
||||
get => ConsoleInteractive.ConsoleSuggestion.Enable24bitColor;
|
||||
set => ConsoleInteractive.ConsoleSuggestion.Enable24bitColor = value;
|
||||
}
|
||||
|
||||
public bool UseBasicArrow
|
||||
{
|
||||
get => ConsoleInteractive.ConsoleSuggestion.UseBasicArrow;
|
||||
set => ConsoleInteractive.ConsoleSuggestion.UseBasicArrow = value;
|
||||
}
|
||||
|
||||
public int SetMaxSuggestionLength(int length)
|
||||
{
|
||||
return ConsoleInteractive.ConsoleSuggestion.SetMaxSuggestionLength(length);
|
||||
}
|
||||
|
||||
public int SetMaxSuggestionCount(int count)
|
||||
{
|
||||
return ConsoleInteractive.ConsoleSuggestion.SetMaxSuggestionCount(count);
|
||||
}
|
||||
|
||||
public bool EnableWriterColor
|
||||
{
|
||||
get => ConsoleInteractive.ConsoleWriter.EnableColor;
|
||||
set => ConsoleInteractive.ConsoleWriter.EnableColor = value;
|
||||
}
|
||||
|
||||
public bool UseVT100ColorCode
|
||||
{
|
||||
get => ConsoleInteractive.ConsoleWriter.UseVT100ColorCode;
|
||||
set => ConsoleInteractive.ConsoleWriter.UseVT100ColorCode = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void ForwardMessage(object? sender, string e)
|
||||
{
|
||||
MessageReceived?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void ForwardInputChange(object? sender, ConsoleInteractive.ConsoleReader.Buffer buffer)
|
||||
{
|
||||
OnInputChange?.Invoke(sender, new ConsoleInputBuffer(buffer.Text, buffer.CursorPosition));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
using Brigadier.NET;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Scripting;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class Debug : Command
|
||||
{
|
||||
public override string CmdName { get { return "debug"; } }
|
||||
public override string CmdUsage { get { return "debug [on|off]"; } }
|
||||
public override string CmdUsage { get { return "debug [on|off|state]"; } }
|
||||
public override string CmdDesc { get { return Translations.cmd_debug_desc; } }
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
|
|
@ -24,6 +28,8 @@ namespace MinecraftClient.Commands
|
|||
.Executes(r => SetDebugMode(r.Source, false, true)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => SetDebugMode(r.Source, false, false)))
|
||||
.Then(l => l.Literal("state")
|
||||
.Executes(r => ShowState(r.Source)))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
|
|
@ -42,15 +48,57 @@ namespace MinecraftClient.Commands
|
|||
|
||||
private int SetDebugMode(CmdResult r, bool flip, bool mode = false)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
|
||||
if (flip)
|
||||
Settings.Config.Logging.DebugMessages = !Settings.Config.Logging.DebugMessages;
|
||||
else
|
||||
Settings.Config.Logging.DebugMessages = mode;
|
||||
|
||||
handler.Log.DebugEnabled = Settings.Config.Logging.DebugMessages;
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_debug_state_on);
|
||||
else
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_debug_state_off);
|
||||
}
|
||||
|
||||
private int ShowState(CmdResult r)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine($"§e=== {Translations.cmd_debug_state_header} ===");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_server,-10}§f{handler.GetServerHost()}:{handler.GetServerPort()}");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_username,-10}§f{handler.GetUsername()}");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_protocol,-10}§f{handler.GetProtocolVersion()}");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_gamemode,-10}§f{handler.GetGamemode()}");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_health,-10}§f{handler.GetHealth():F1}");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_food,-10}§f{handler.GetSaturation()}");
|
||||
|
||||
var loc = handler.GetCurrentLocation();
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_location,-10}§f{loc.X:F2}, {loc.Y:F2}, {loc.Z:F2}");
|
||||
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_tps,-10}§f{handler.GetServerTPS():F1}");
|
||||
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_console,-10}§f{(ConsoleIO.Backend?.GetType().Name ?? "null")}");
|
||||
|
||||
var features = new StringBuilder();
|
||||
features.Append(handler.GetTerrainEnabled() ? "§aTerrain " : "§8Terrain ");
|
||||
features.Append(handler.GetInventoryEnabled() ? "§aInventory " : "§8Inventory ");
|
||||
features.Append(handler.GetEntityHandlingEnabled() ? "§aEntity " : "§8Entity ");
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_features,-10}{features}");
|
||||
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_debug,-10}§f{(Settings.Config.Logging.DebugMessages ? "§aON" : "§cOFF")}");
|
||||
|
||||
var bots = handler.GetLoadedChatBots();
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_bots} ({bots.Count}): §f{string.Join(", ", bots.Select(b => b.GetType().Name))}");
|
||||
|
||||
var players = handler.GetOnlinePlayers();
|
||||
sb.AppendLine($"§7{Translations.cmd_debug_state_players,-10}§f{string.Format(Translations.cmd_debug_state_online, players.Length)}");
|
||||
|
||||
handler.Log.Info(sb.ToString());
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
|
@ -6,6 +6,7 @@ using Brigadier.NET;
|
|||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Tui;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
|
|
@ -22,6 +23,8 @@ namespace MinecraftClient.Commands
|
|||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Then(l => l.Literal("list")
|
||||
.Executes(r => GetUsage(r.Source, "list")))
|
||||
.Then(l => l.Literal("open")
|
||||
.Executes(r => GetUsage(r.Source, "open")))
|
||||
.Then(l => l.Literal("close")
|
||||
.Executes(r => GetUsage(r.Source, "close")))
|
||||
.Then(l => l.Literal("click")
|
||||
|
|
@ -59,6 +62,9 @@ namespace MinecraftClient.Commands
|
|||
.Then(l => l.Argument("Count", Arguments.Integer(0, 64))
|
||||
.Executes(r => SearchItem(r.Source, MccArguments.GetItemType(r, "ItemType"), Arguments.GetInteger(r, "Count"))))))
|
||||
.Then(l => l.Argument("InventoryId", MccArguments.InventoryId())
|
||||
.Executes(r => DoOpenOrList(r.Source, Arguments.GetInteger(r, "InventoryId")))
|
||||
.Then(l => l.Literal("open")
|
||||
.Executes(r => DoOpenTui(r.Source, Arguments.GetInteger(r, "InventoryId"))))
|
||||
.Then(l => l.Literal("close")
|
||||
.Executes(r => DoCloseAction(r.Source, Arguments.GetInteger(r, "InventoryId"))))
|
||||
.Then(l => l.Literal("list")
|
||||
|
|
@ -113,6 +119,7 @@ namespace MinecraftClient.Commands
|
|||
return r.SetAndReturn(cmd switch
|
||||
{
|
||||
#pragma warning disable format // @formatter:off
|
||||
"open" => Translations.cmd_inventory_help_open + usageStr + "/inventory <id> open",
|
||||
"list" => Translations.cmd_inventory_help_list + usageStr + "/inventory <player|container|<id>> list",
|
||||
"close" => Translations.cmd_inventory_help_close + usageStr + "/inventory <player|container|<id>> close",
|
||||
"click" => Translations.cmd_inventory_help_click + usageStr + "/inventory <player|container|<id>> click <slot> [left|right|middle|shift|shiftright]\nDefault is left click",
|
||||
|
|
@ -394,6 +401,55 @@ namespace MinecraftClient.Commands
|
|||
}
|
||||
|
||||
|
||||
private int DoOpenOrList(CmdResult r, int inventoryId)
|
||||
{
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend)
|
||||
return DoOpenTui(r, inventoryId);
|
||||
return DoListAction(r, inventoryId);
|
||||
}
|
||||
|
||||
private int DoOpenTui(CmdResult r, int inventoryId)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
|
||||
if (!handler.GetInventoryEnabled())
|
||||
return r.SetAndReturn(CmdResult.Status.FailNeedInventory);
|
||||
|
||||
if (ConsoleIO.Backend is not TuiConsoleBackend)
|
||||
{
|
||||
handler.Log.Warn(Translations.cmd_inventory_tui_only);
|
||||
return r.SetAndReturn(CmdResult.Status.Fail);
|
||||
}
|
||||
|
||||
if (InventoryTuiHost.IsRunning)
|
||||
{
|
||||
handler.Log.Warn(Translations.cmd_inventory_tui_already_running);
|
||||
return r.SetAndReturn(CmdResult.Status.Fail);
|
||||
}
|
||||
|
||||
var container = handler.GetInventory(inventoryId);
|
||||
if (container == null)
|
||||
{
|
||||
string msg = string.Format(Translations.cmd_inventory_not_exist, inventoryId);
|
||||
handler.Log.Warn(msg);
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, msg);
|
||||
}
|
||||
|
||||
handler.Log.Info(string.Format(Translations.cmd_inventory_tui_opening, inventoryId));
|
||||
|
||||
bool success = InventoryTuiHost.Launch(handler, inventoryId);
|
||||
if (success)
|
||||
{
|
||||
handler.Log.Info(Translations.cmd_inventory_tui_opened);
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
else
|
||||
{
|
||||
handler.Log.Warn(Translations.cmd_inventory_tui_launch_failed);
|
||||
return r.SetAndReturn(CmdResult.Status.Fail);
|
||||
}
|
||||
}
|
||||
|
||||
#region Methods for commands help
|
||||
|
||||
private static string GetAvailableActions()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
|
@ -8,6 +8,7 @@ using Brigadier.NET;
|
|||
using FuzzySharp;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Scripting;
|
||||
using MinecraftClient.Tui;
|
||||
using static MinecraftClient.Settings;
|
||||
|
||||
namespace MinecraftClient
|
||||
|
|
@ -16,18 +17,23 @@ namespace MinecraftClient
|
|||
/// Allows simultaneous console input and output without breaking user input
|
||||
/// (Without having this annoying behaviour : User inp[Some Console output]ut)
|
||||
/// Provide some fancy features such as formatted output, text pasting and tab-completion.
|
||||
/// By ORelio - (c) 2012-2018 - Available under the CDDL-1.0 license
|
||||
/// By ORelio - (c) 2012-2018 - Available under the CDDL-1.0 License
|
||||
/// </summary>
|
||||
public static class ConsoleIO
|
||||
{
|
||||
private static IAutoComplete? autocomplete_engine;
|
||||
|
||||
/// <summary>
|
||||
/// The active console backend. Set once during startup.
|
||||
/// </summary>
|
||||
public static IConsoleBackend Backend { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Reset the IO mechanism and clear all buffers
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
ClearLineAndBuffer();
|
||||
Backend?.ClearInputBuffer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -40,9 +46,8 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether to use interactive IO or basic IO.
|
||||
/// Set to true to disable interactive command prompt and use the default Console.Read|Write() methods.
|
||||
/// Color codes are printed as is when BasicIO is enabled.
|
||||
/// Determines whether to use basic IO (legacy flag, kept for compatibility).
|
||||
/// In the new architecture this is true when Backend is BasicConsoleBackend.
|
||||
/// </summary>
|
||||
public static bool BasicIO = false;
|
||||
|
||||
|
|
@ -59,7 +64,7 @@ namespace MinecraftClient
|
|||
/// <summary>
|
||||
/// Specify a generic log line prefix for WriteLogLine()
|
||||
/// </summary>
|
||||
public static string LogPrefix = "§8[Log] ";
|
||||
public static string LogPrefix = "§8[MCC] ";
|
||||
|
||||
/// <summary>
|
||||
/// Read a password from the standard input
|
||||
|
|
@ -68,13 +73,7 @@ namespace MinecraftClient
|
|||
{
|
||||
if (BasicIO)
|
||||
return Console.ReadLine();
|
||||
else
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(false);
|
||||
var input = ConsoleInteractive.ConsoleReader.RequestImmediateInput();
|
||||
ConsoleInteractive.ConsoleReader.SetInputVisible(true);
|
||||
return input;
|
||||
}
|
||||
return Backend.ReadPassword();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -84,8 +83,7 @@ namespace MinecraftClient
|
|||
{
|
||||
if (BasicIO)
|
||||
return Console.ReadLine() ?? String.Empty;
|
||||
else
|
||||
return ConsoleInteractive.ConsoleReader.RequestImmediateInput();
|
||||
return Backend.RequestImmediateInput();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -109,7 +107,7 @@ namespace MinecraftClient
|
|||
if (BasicIO)
|
||||
Console.WriteLine(line);
|
||||
else
|
||||
ConsoleInteractive.ConsoleWriter.WriteLine(line);
|
||||
Backend.WriteLine(line);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -153,7 +151,7 @@ namespace MinecraftClient
|
|||
return;
|
||||
}
|
||||
output.Append(str);
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString());
|
||||
Backend.WriteLineFormatted(output.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,10 +175,9 @@ namespace MinecraftClient
|
|||
private static void ClearLineAndBuffer()
|
||||
{
|
||||
if (BasicIO) return;
|
||||
ConsoleInteractive.ConsoleReader.ClearBuffer();
|
||||
Backend.ClearInputBuffer();
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
internal static bool AutoCompleteDone = false;
|
||||
|
|
@ -193,12 +190,37 @@ namespace MinecraftClient
|
|||
private static Task _latestTask = Task.CompletedTask;
|
||||
private static CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
private static void MccAutocompleteHandler(ConsoleInteractive.ConsoleReader.Buffer buffer)
|
||||
private static void SendSuggestions(
|
||||
ConsoleInteractive.ConsoleSuggestion.Suggestion[] classicSugs,
|
||||
Tuple<int, int> range)
|
||||
{
|
||||
if (Backend is ClassicConsoleBackend classic)
|
||||
{
|
||||
classic.UpdateSuggestions(classicSugs, range);
|
||||
}
|
||||
else if (Backend is TuiConsoleBackend tui)
|
||||
{
|
||||
var tuiSugs = new CommandSuggestion[classicSugs.Length];
|
||||
for (int i = 0; i < classicSugs.Length; i++)
|
||||
tuiSugs[i] = new CommandSuggestion(classicSugs[i].Text, classicSugs[i].Tooltip);
|
||||
tui.UpdateSuggestions(tuiSugs, (range.Item1, range.Item2));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoClearSuggestions()
|
||||
{
|
||||
if (Backend is ClassicConsoleBackend classic)
|
||||
classic.ClearSuggestions();
|
||||
else if (Backend is TuiConsoleBackend tui)
|
||||
tui.ClearSuggestions();
|
||||
}
|
||||
|
||||
private static void MccAutocompleteHandler(ConsoleInputBuffer buffer)
|
||||
{
|
||||
string fullCommand = buffer.Text;
|
||||
if (string.IsNullOrEmpty(fullCommand))
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +230,7 @@ namespace MinecraftClient
|
|||
int offset = InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none ? 0 : 1;
|
||||
if (buffer.CursorPosition - offset < 0)
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
|
@ -232,7 +254,7 @@ namespace MinecraftClient
|
|||
foreach (var cmd in Commands)
|
||||
sugList.Add(new(cmd));
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(sugList.ToArray(), new(offset, offset));
|
||||
SendSuggestions(sugList.ToArray(), new(offset, offset));
|
||||
}
|
||||
else if (command.Length > 0 && command[0] == '/' && !command.Contains(' '))
|
||||
{
|
||||
|
|
@ -242,7 +264,7 @@ namespace MinecraftClient
|
|||
int index = 0;
|
||||
foreach (var sug in sorted)
|
||||
sugList[index++] = new(sug.Value);
|
||||
ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(sugList, new(offset, offset + command.Length));
|
||||
SendSuggestions(sugList, new(offset, offset + command.Length));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -257,7 +279,7 @@ namespace MinecraftClient
|
|||
int sugLen = suggestions.List.Count;
|
||||
if (sugLen == 0)
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +300,7 @@ namespace MinecraftClient
|
|||
foreach (var sug in sorted)
|
||||
sugList[index++] = new(sug.Value, dictionary[sug.Value] ?? string.Empty);
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.UpdateSuggestions(sugList, range);
|
||||
SendSuggestions(sugList, range);
|
||||
}
|
||||
}, cts.Token);
|
||||
_latestTask = newTask;
|
||||
|
|
@ -287,22 +309,68 @@ namespace MinecraftClient
|
|||
}
|
||||
else
|
||||
{
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AutocompleteHandler(object? sender, ConsoleInteractive.ConsoleReader.Buffer buffer)
|
||||
public static void AutocompleteHandler(object? sender, ConsoleInputBuffer buffer)
|
||||
{
|
||||
if (Settings.Config.Console.CommandSuggestion.Enable)
|
||||
MccAutocompleteHandler(buffer);
|
||||
}
|
||||
|
||||
private static readonly string[] OfflineCommands = ["quit", "exit", "connect", "reco", "help"];
|
||||
|
||||
public static void OfflineAutocompleteHandler(object? sender, ConsoleInputBuffer buffer)
|
||||
{
|
||||
if (!Settings.Config.Console.CommandSuggestion.Enable)
|
||||
return;
|
||||
|
||||
string fullCommand = buffer.Text;
|
||||
if (string.IsNullOrEmpty(fullCommand))
|
||||
{
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
var InternalCmdChar = Config.Main.Advanced.InternalCmdChar;
|
||||
int offset = 0;
|
||||
if (InternalCmdChar != MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none)
|
||||
{
|
||||
if (fullCommand[0] != InternalCmdChar.ToChar())
|
||||
{
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
offset = 1;
|
||||
}
|
||||
|
||||
string command = fullCommand[offset..];
|
||||
if (command.Contains(' '))
|
||||
{
|
||||
DoClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
var sugList = new List<ConsoleInteractive.ConsoleSuggestion.Suggestion>();
|
||||
foreach (string cmd in OfflineCommands)
|
||||
{
|
||||
if (command.Length == 0 || cmd.StartsWith(command, StringComparison.OrdinalIgnoreCase))
|
||||
sugList.Add(new(cmd));
|
||||
}
|
||||
|
||||
if (sugList.Count > 0)
|
||||
SendSuggestions(sugList.ToArray(), new(offset, offset + command.Length));
|
||||
else
|
||||
DoClearSuggestions();
|
||||
}
|
||||
|
||||
public static void CancelAutocomplete()
|
||||
{
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_latestTask = Task.CompletedTask;
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
DoClearSuggestions();
|
||||
|
||||
AutoCompleteDone = false;
|
||||
AutoCompleteResult = Array.Empty<string>();
|
||||
|
|
|
|||
71
MinecraftClient/IConsoleBackend.cs
Normal file
71
MinecraftClient/IConsoleBackend.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Input buffer state passed with OnInputChange events.
|
||||
/// </summary>
|
||||
public readonly struct ConsoleInputBuffer
|
||||
{
|
||||
public string Text { get; }
|
||||
public int CursorPosition { get; }
|
||||
|
||||
public ConsoleInputBuffer(string text, int cursorPosition)
|
||||
{
|
||||
Text = text;
|
||||
CursorPosition = cursorPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backend-independent suggestion item used by the TUI autocomplete popup.
|
||||
/// Mirrors the shape of ConsoleInteractive.ConsoleSuggestion.Suggestion
|
||||
/// without requiring a dependency on the ConsoleInteractive assembly.
|
||||
/// </summary>
|
||||
public readonly struct CommandSuggestion
|
||||
{
|
||||
public string Text { get; }
|
||||
public string Tooltip { get; }
|
||||
|
||||
public CommandSuggestion(string text, string tooltip = "")
|
||||
{
|
||||
Text = text;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction over the console I/O backend.
|
||||
/// Implementations: ClassicConsoleBackend (ConsoleInteractive), TuiConsoleBackend (Avalonia/Consolonia), BasicConsoleBackend (stdio).
|
||||
/// </summary>
|
||||
public interface IConsoleBackend
|
||||
{
|
||||
void Init();
|
||||
|
||||
void WriteLine(string text);
|
||||
|
||||
void WriteLineFormatted(string text);
|
||||
|
||||
void BeginReadThread();
|
||||
|
||||
void StopReadThread();
|
||||
|
||||
event EventHandler<string>? MessageReceived;
|
||||
|
||||
event EventHandler<ConsoleInputBuffer>? OnInputChange;
|
||||
|
||||
string RequestImmediateInput();
|
||||
|
||||
string? ReadPassword();
|
||||
|
||||
void ClearInputBuffer();
|
||||
|
||||
bool DisplayUserInput { get; set; }
|
||||
|
||||
void SetInputVisible(bool visible);
|
||||
|
||||
void SetBackreadBufferLimit(int limit);
|
||||
|
||||
void Shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -260,9 +260,9 @@ namespace MinecraftClient
|
|||
Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
|
||||
cmdprompt = new CancellationTokenSource();
|
||||
ConsoleInteractive.ConsoleReader.BeginReadThread();
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived += ConsoleReaderOnMessageReceived;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange += ConsoleIO.AutocompleteHandler;
|
||||
ConsoleIO.Backend.BeginReadThread();
|
||||
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -304,9 +304,9 @@ namespace MinecraftClient
|
|||
}
|
||||
else if (InternalConfig.InteractiveMode)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
Program.HandleFailure();
|
||||
}
|
||||
|
||||
|
|
@ -324,9 +324,9 @@ namespace MinecraftClient
|
|||
// kick messages and Ignore_Kick_Message is false, or retry limit reached)
|
||||
if (InternalConfig.InteractiveMode)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
Program.HandleFailure();
|
||||
}
|
||||
|
||||
|
|
@ -383,6 +383,11 @@ namespace MinecraftClient
|
|||
|
||||
UpdateKeepAlive();
|
||||
Log.Info($"Successfully transferred connection and logged in to {newHost}:{newPort}.");
|
||||
|
||||
cmdprompt = new CancellationTokenSource();
|
||||
ConsoleIO.Backend.BeginReadThread();
|
||||
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -426,9 +431,9 @@ namespace MinecraftClient
|
|||
}
|
||||
else if (InternalConfig.InteractiveMode)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
Program.HandleFailure();
|
||||
}
|
||||
|
||||
|
|
@ -683,6 +688,8 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
instance = null;
|
||||
|
||||
DispatchBotEvent(bot => bot.OnDisconnect(ChatBot.DisconnectReason.UserLogout, ""));
|
||||
|
||||
botsOnHold.Clear();
|
||||
|
|
@ -715,6 +722,8 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public void OnConnectionLost(ChatBot.DisconnectReason reason, string message)
|
||||
{
|
||||
instance = null;
|
||||
|
||||
ConsoleIO.CancelAutocomplete();
|
||||
|
||||
handler.Dispose();
|
||||
|
|
@ -775,9 +784,9 @@ namespace MinecraftClient
|
|||
|
||||
if (!will_restart)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleInteractive.ConsoleReader.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleInteractive.ConsoleReader.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
|
||||
Program.HandleFailure(null, false, reason);
|
||||
}
|
||||
}
|
||||
|
|
@ -803,6 +812,14 @@ namespace MinecraftClient
|
|||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the console message handler delegate for re-attaching after TUI mode.
|
||||
/// </summary>
|
||||
public EventHandler<string> GetConsoleMessageHandler()
|
||||
{
|
||||
return ConsoleReaderOnMessageReceived;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the user to send chat messages, commands, and leave the server.
|
||||
/// Process text from the MCC command prompt on the main thread.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Brigadier.NET" Version="1.2.13" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.143.0" />
|
||||
<PackageReference Include="Consolonia" Version="11.3.9" />
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.5.1" />
|
||||
<PackageReference Include="DynamicExpresso.Core" Version="2.19.3" />
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ namespace MinecraftClient.Physics
|
|||
using var stream = assembly.GetManifestResourceStream("BlockShapeData.json");
|
||||
if (stream is null)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted("§e[Physics] BlockShapeData.json not found as embedded resource");
|
||||
ConsoleIO.WriteLineFormatted("§e[Physics] BlockShapeData.json not found as embedded resource");
|
||||
return;
|
||||
}
|
||||
using var doc = JsonDocument.Parse(stream);
|
||||
|
|
@ -130,7 +130,7 @@ namespace MinecraftClient.Physics
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted($"§e[Physics] Failed to load BlockShapeData.json: {ex.Message}");
|
||||
ConsoleIO.WriteLineFormatted($"§e[Physics] Failed to load BlockShapeData.json: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ namespace MinecraftClient
|
|||
static void Main(string[] args)
|
||||
{
|
||||
// [SENTRY] Initialize Sentry SDK only if the DSN is not empty
|
||||
if (SentryDSN != string.Empty) {
|
||||
if (SentryDSN != string.Empty)
|
||||
{
|
||||
_sentrySdk = SentrySdk.Init(options =>
|
||||
{
|
||||
options.Dsn = SentryDSN;
|
||||
|
|
@ -73,7 +74,7 @@ namespace MinecraftClient
|
|||
options.TracesSampleRate = 1.0;
|
||||
options.SendDefaultPii = false;
|
||||
});
|
||||
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
||||
{
|
||||
SentrySdk.CaptureException((Exception)eventArgs.ExceptionObject);
|
||||
|
|
@ -115,7 +116,10 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
if (!ConsoleIO.BasicIO)
|
||||
ConsoleInteractive.ConsoleWriter.Init();
|
||||
{
|
||||
ConsoleIO.Backend = new ClassicConsoleBackend();
|
||||
ConsoleIO.Backend.Init();
|
||||
}
|
||||
|
||||
ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam");
|
||||
|
||||
|
|
@ -164,20 +168,24 @@ namespace MinecraftClient
|
|||
|
||||
// Only show the Sentry message if the DSN is not empty
|
||||
// as Sentry will not be initialized if the DSN is empty
|
||||
if (SentryDSN != string.Empty) {
|
||||
if (SentryDSN != string.Empty)
|
||||
{
|
||||
ConsoleIO.WriteLine(Translations.mcc_sentry_logging);
|
||||
}
|
||||
}
|
||||
else if (!loadSucceed)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleIO.Backend?.StopReadThread();
|
||||
string command = " ";
|
||||
while (command.Length > 0)
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Empty);
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_invaild_config, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
|
||||
command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim();
|
||||
if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
else
|
||||
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
|
||||
command = ConsoleIO.ReadLine().Trim();
|
||||
if (command.Length > 0)
|
||||
{
|
||||
if (Config.Main.Advanced.InternalCmdChar.ToChar() != ' '
|
||||
|
|
@ -210,11 +218,31 @@ namespace MinecraftClient
|
|||
ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl));
|
||||
WriteBackSettings(true); // format
|
||||
}
|
||||
|
||||
|
||||
if (!Config.Main.Advanced.EnableSentry)
|
||||
_sentrySdk?.Dispose();
|
||||
}
|
||||
|
||||
// Switch to TUI mode if configured (must happen after config load)
|
||||
if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui)
|
||||
{
|
||||
ConsoleIO.Backend?.Shutdown();
|
||||
var tuiBackend = new Tui.TuiConsoleBackend();
|
||||
ConsoleIO.Backend = tuiBackend;
|
||||
tuiBackend.RunTuiMainLoop(args);
|
||||
return;
|
||||
}
|
||||
|
||||
ContinueAfterTuiInit(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continues MCC startup after console mode has been determined.
|
||||
/// Called directly from Main for classic/basic mode, or from a background
|
||||
/// thread for TUI mode (after the Avalonia UI loop has started).
|
||||
/// </summary>
|
||||
internal static void ContinueAfterTuiInit(string[] args)
|
||||
{
|
||||
//Other command-line arguments
|
||||
if (args.Length >= 1)
|
||||
{
|
||||
|
|
@ -668,7 +696,7 @@ namespace MinecraftClient
|
|||
{
|
||||
// [SENTRY]
|
||||
SentrySdk.CaptureException(e);
|
||||
|
||||
|
||||
ConsoleIO.WriteLine(e.Message);
|
||||
ConsoleIO.WriteLine(e.StackTrace ?? "");
|
||||
HandleFailure(); // Other error
|
||||
|
|
@ -723,11 +751,15 @@ namespace MinecraftClient
|
|||
/// <param name="keepAccountAndServerSettings">Optional, keep account and server settings</param>
|
||||
public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
new Thread(new ThreadStart(delegate
|
||||
{
|
||||
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
|
||||
if (offlinePrompt is not null) { offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); }
|
||||
if (offlinePrompt is not null)
|
||||
{
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
|
||||
offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
|
||||
}
|
||||
if (delaySeconds > 0)
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, delaySeconds));
|
||||
|
|
@ -742,11 +774,15 @@ namespace MinecraftClient
|
|||
public static void DoExit(int exitcode = 0)
|
||||
{
|
||||
WriteBackSettings();
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
ConsoleIO.Backend?.Shutdown();
|
||||
ConsoleIO.WriteLineFormatted("§a" + string.Format(Translations.config_saving, settingsIniPath));
|
||||
|
||||
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
|
||||
if (offlinePrompt is not null) { offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset(); }
|
||||
if (offlinePrompt is not null)
|
||||
{
|
||||
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
|
||||
offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
|
||||
}
|
||||
if (Config.Main.Advanced.PlayerHeadAsIcon) { ConsoleIcon.RevertToMCCIcon(); }
|
||||
Environment.Exit(exitcode);
|
||||
}
|
||||
|
|
@ -771,10 +807,12 @@ namespace MinecraftClient
|
|||
if (!String.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
ConsoleIO.Reset();
|
||||
try {
|
||||
try
|
||||
{
|
||||
while (Console.KeyAvailable)
|
||||
Console.ReadKey(true);
|
||||
} catch { }
|
||||
}
|
||||
catch { }
|
||||
ConsoleIO.WriteLine(errorMessage);
|
||||
|
||||
if (disconnectReason.HasValue)
|
||||
|
|
@ -789,7 +827,7 @@ namespace MinecraftClient
|
|||
if (versionError)
|
||||
{
|
||||
ConsoleIO.WriteLine(Translations.mcc_server_version);
|
||||
InternalConfig.MinecraftVersion = ConsoleInteractive.ConsoleReader.RequestImmediateInput();
|
||||
InternalConfig.MinecraftVersion = ConsoleIO.ReadLine();
|
||||
if (InternalConfig.MinecraftVersion != "")
|
||||
{
|
||||
useMcVersionOnce = true;
|
||||
|
|
@ -798,14 +836,16 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
if (disconnectReason.HasValue) {
|
||||
if (disconnectReason.HasValue)
|
||||
{
|
||||
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
|
||||
return; //AutoRelog is triggering a restart of the client, don't turn on the offline prompt
|
||||
}
|
||||
|
||||
|
||||
if (offlinePrompt is null)
|
||||
{
|
||||
ConsoleInteractive.ConsoleReader.StopReadThread();
|
||||
ConsoleIO.Backend.StopReadThread();
|
||||
ConsoleIO.Backend.OnInputChange += ConsoleIO.OfflineAutocompleteHandler;
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
offlinePrompt = new(new Thread(new ThreadStart(delegate
|
||||
|
|
@ -814,7 +854,10 @@ namespace MinecraftClient
|
|||
string command = " ";
|
||||
ConsoleIO.WriteLine(string.Empty);
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
|
||||
if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
else
|
||||
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
|
||||
|
||||
while (!cancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
|
|
@ -826,7 +869,7 @@ namespace MinecraftClient
|
|||
if (cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
command = ConsoleInteractive.ConsoleReader.RequestImmediateInput().Trim();
|
||||
command = ConsoleIO.ReadLine().Trim();
|
||||
if (command.Length > 0)
|
||||
{
|
||||
string message = "";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
|
|
@ -1392,6 +1392,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Console mode: "classic" for the standard terminal, "tui" for a pseudo-graphical full-screen interface..
|
||||
/// </summary>
|
||||
internal static string Console_General_ConsoleMode {
|
||||
get {
|
||||
return ResourceManager.GetString("Console.General.ConsoleMode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use "disable", "legacy_4bit", "vt100_4bit", "vt100_8bit" or "vt100_24bit". If a garbled code like "←[0m" appears on the terminal, you can try switching to "legacy_4bit" mode, or just disable it..
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
|
@ -554,6 +554,9 @@ Custom colors are only available when using "vt100_24bit" color mode.</value>
|
|||
<data name="Console.CommandSuggestion.Use_Basic_Arrow" xml:space="preserve">
|
||||
<value>Enable this option if the arrows in the command suggestions are not displayed properly in your terminal.</value>
|
||||
</data>
|
||||
<data name="Console.General.ConsoleMode" xml:space="preserve">
|
||||
<value>Console mode: "classic" for the standard terminal, "tui" for a pseudo-graphical full-screen interface.</value>
|
||||
</data>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
|
|
@ -6133,5 +6133,383 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("proxy.connected", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use '{0}quit' to exit Minecraft Console Client..
|
||||
/// </summary>
|
||||
internal static string mcc_use_quit_to_exit {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.use_quit_to_exit", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to MCC Debug State.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Server.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_server {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_server", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Username.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_username {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_username", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Protocol.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_protocol {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_protocol", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to GameMode.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_gamemode {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_gamemode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Health.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_health {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_health", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Food.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_food {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_food", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Location.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_location {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_location", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to TPS.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_tps {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_tps", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Console.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_console {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_console", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Features.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_features {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_features", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Debug.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_debug {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_debug", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bots.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_bots {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_bots", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Players.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_players {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_players", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} online.
|
||||
/// </summary>
|
||||
internal static string cmd_debug_state_online {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.debug.state_online", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open interactive TUI inventory viewer (TUI mode only)..
|
||||
/// </summary>
|
||||
internal static string cmd_inventory_help_open {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.inventory.help.open", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Interactive TUI is only available in TUI console mode. Use '/inventory <id> list' instead..
|
||||
/// </summary>
|
||||
internal static string cmd_inventory_tui_only {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.inventory.tui_only", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to TUI inventory viewer is already running..
|
||||
/// </summary>
|
||||
internal static string cmd_inventory_tui_already_running {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.inventory.tui_already_running", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Opening TUI for Inventory #{0}....
|
||||
/// </summary>
|
||||
internal static string cmd_inventory_tui_opening {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.inventory.tui_opening", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Inventory dialog opened..
|
||||
/// </summary>
|
||||
internal static string cmd_inventory_tui_opened {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.inventory.tui_opened", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to launch TUI inventory viewer..
|
||||
/// </summary>
|
||||
internal static string cmd_inventory_tui_launch_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.inventory.tui_launch_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hold Shift + Left-click to select and copy text..
|
||||
/// </summary>
|
||||
internal static string tui_select_copy_hint {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.select_copy_hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Input cleared. Press Ctrl+C again to quit..
|
||||
/// </summary>
|
||||
internal static string tui_ctrlc_input_cleared {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.ctrlc_input_cleared", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Press Ctrl+C again to quit MCC..
|
||||
/// </summary>
|
||||
internal static string tui_ctrlc_quit_hint {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.ctrlc_quit_hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [ Item Info ].
|
||||
/// </summary>
|
||||
internal static string tui_inventory_item_info {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.item_info", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [ Held Item ].
|
||||
/// </summary>
|
||||
internal static string tui_inventory_held_item {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.held_item", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [ Controls ].
|
||||
/// </summary>
|
||||
internal static string tui_inventory_controls {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.controls", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to LClick Pick/Place....
|
||||
/// </summary>
|
||||
internal static string tui_inventory_controls_help {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.controls_help", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to (empty).
|
||||
/// </summary>
|
||||
internal static string tui_inventory_cursor_empty {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.cursor_empty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Off.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_offhand {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.offhand", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Out.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_output {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.output", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hd.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_equip_head {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.equip_head", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Bd.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_equip_body {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.equip_body", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Lg.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_equip_legs {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.equip_legs", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ft.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_equip_feet {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.equip_feet", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hover over a slot to see item details..
|
||||
/// </summary>
|
||||
internal static string tui_inventory_hover_hint {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.hover_hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to (Empty).
|
||||
/// </summary>
|
||||
internal static string tui_inventory_slot_empty {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.slot_empty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Slot #{0} Count: {1}.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_slot_detail {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.slot_detail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Container not found.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_container_not_found {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.container_not_found", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Inventory #{0} - {1}.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_title {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} items.
|
||||
/// </summary>
|
||||
internal static string tui_inventory_item_count {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.inventory.item_count", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2163,4 +2163,137 @@ Logging in...</value>
|
|||
<data name="mcc.sentry_logging" xml:space="preserve">
|
||||
<value>MCC uses Sentry to log errors. You can opt-out by setting the EnableSentry option in the configuration file to false.</value>
|
||||
</data>
|
||||
<data name="mcc.use_quit_to_exit" xml:space="preserve">
|
||||
<value>Use '{0}quit' to exit Minecraft Console Client.</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_header" xml:space="preserve">
|
||||
<value>MCC Debug State</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_server" xml:space="preserve">
|
||||
<value>Server</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_username" xml:space="preserve">
|
||||
<value>Username</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_protocol" xml:space="preserve">
|
||||
<value>Protocol</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_gamemode" xml:space="preserve">
|
||||
<value>GameMode</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_health" xml:space="preserve">
|
||||
<value>Health</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_food" xml:space="preserve">
|
||||
<value>Food</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_location" xml:space="preserve">
|
||||
<value>Location</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_tps" xml:space="preserve">
|
||||
<value>TPS</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_console" xml:space="preserve">
|
||||
<value>Console</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_features" xml:space="preserve">
|
||||
<value>Features</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_debug" xml:space="preserve">
|
||||
<value>Debug</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_bots" xml:space="preserve">
|
||||
<value>Bots</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_players" xml:space="preserve">
|
||||
<value>Players</value>
|
||||
</data>
|
||||
<data name="cmd.debug.state_online" xml:space="preserve">
|
||||
<value>{0} online</value>
|
||||
</data>
|
||||
<data name="cmd.inventory.help.open" xml:space="preserve">
|
||||
<value>Open interactive TUI inventory viewer (TUI mode only).</value>
|
||||
</data>
|
||||
<data name="cmd.inventory.tui_only" xml:space="preserve">
|
||||
<value>Interactive TUI is only available in TUI console mode. Use '/inventory <id> list' instead.</value>
|
||||
</data>
|
||||
<data name="cmd.inventory.tui_already_running" xml:space="preserve">
|
||||
<value>TUI inventory viewer is already running.</value>
|
||||
</data>
|
||||
<data name="cmd.inventory.tui_opening" xml:space="preserve">
|
||||
<value>Opening TUI for Inventory #{0}...</value>
|
||||
</data>
|
||||
<data name="cmd.inventory.tui_opened" xml:space="preserve">
|
||||
<value>Inventory dialog opened.</value>
|
||||
</data>
|
||||
<data name="cmd.inventory.tui_launch_failed" xml:space="preserve">
|
||||
<value>Failed to launch TUI inventory viewer.</value>
|
||||
</data>
|
||||
<data name="tui.select_copy_hint" xml:space="preserve">
|
||||
<value>Hold Shift + Left-click to select and copy text.</value>
|
||||
</data>
|
||||
<data name="tui.ctrlc_input_cleared" xml:space="preserve">
|
||||
<value>Input cleared. Press Ctrl+C again to quit.</value>
|
||||
</data>
|
||||
<data name="tui.ctrlc_quit_hint" xml:space="preserve">
|
||||
<value>Press Ctrl+C again to quit MCC.</value>
|
||||
</data>
|
||||
<data name="tui.inventory.item_info" xml:space="preserve">
|
||||
<value>[ Item Info ]</value>
|
||||
</data>
|
||||
<data name="tui.inventory.held_item" xml:space="preserve">
|
||||
<value>[ Held Item ]</value>
|
||||
</data>
|
||||
<data name="tui.inventory.controls" xml:space="preserve">
|
||||
<value>[ Controls ]</value>
|
||||
</data>
|
||||
<data name="tui.inventory.controls_help" xml:space="preserve">
|
||||
<value>LClick Pick/Place
|
||||
RClick Half/Place1
|
||||
Shift+C QuickMove
|
||||
Q Drop x1
|
||||
Ctrl+Q Drop Stack
|
||||
R Refresh
|
||||
E/ESC Exit</value>
|
||||
</data>
|
||||
<data name="tui.inventory.cursor_empty" xml:space="preserve">
|
||||
<value>(empty)</value>
|
||||
</data>
|
||||
<data name="tui.inventory.offhand" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="tui.inventory.output" xml:space="preserve">
|
||||
<value>Out</value>
|
||||
</data>
|
||||
<data name="tui.inventory.equip_head" xml:space="preserve">
|
||||
<value>Hd</value>
|
||||
</data>
|
||||
<data name="tui.inventory.equip_body" xml:space="preserve">
|
||||
<value>Bd</value>
|
||||
</data>
|
||||
<data name="tui.inventory.equip_legs" xml:space="preserve">
|
||||
<value>Lg</value>
|
||||
</data>
|
||||
<data name="tui.inventory.equip_feet" xml:space="preserve">
|
||||
<value>Ft</value>
|
||||
</data>
|
||||
<data name="tui.inventory.hover_hint" xml:space="preserve">
|
||||
<value>Hover over a slot to
|
||||
see item details.</value>
|
||||
</data>
|
||||
<data name="tui.inventory.slot_empty" xml:space="preserve">
|
||||
<value>(Empty)</value>
|
||||
</data>
|
||||
<data name="tui.inventory.slot_detail" xml:space="preserve">
|
||||
<value>Slot #{0} Count: {1}</value>
|
||||
</data>
|
||||
<data name="tui.inventory.container_not_found" xml:space="preserve">
|
||||
<value>Container not found</value>
|
||||
</data>
|
||||
<data name="tui.inventory.title" xml:space="preserve">
|
||||
<value>Inventory #{0} - {1}</value>
|
||||
</data>
|
||||
<data name="tui.inventory.item_count" xml:space="preserve">
|
||||
<value>{0} items</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
|
@ -1086,36 +1086,29 @@ namespace MinecraftClient
|
|||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
// Reader
|
||||
ConsoleInteractive.ConsoleReader.DisplayUesrInput = General.Display_Input;
|
||||
var backend = ConsoleIO.Backend;
|
||||
if (backend == null) return;
|
||||
|
||||
// Writer
|
||||
ConsoleInteractive.ConsoleWriter.EnableColor = General.ConsoleColorMode != ConsoleColorModeType.disable;
|
||||
backend.DisplayUserInput = General.Display_Input;
|
||||
backend.SetBackreadBufferLimit(General.History_Input_Records);
|
||||
|
||||
ConsoleInteractive.ConsoleWriter.UseVT100ColorCode = General.ConsoleColorMode != ConsoleColorModeType.legacy_4bit;
|
||||
|
||||
// Buffer
|
||||
General.History_Input_Records =
|
||||
ConsoleInteractive.ConsoleBuffer.SetBackreadBufferLimit(General.History_Input_Records);
|
||||
|
||||
// Suggestion
|
||||
if (General.ConsoleColorMode == ConsoleColorModeType.disable)
|
||||
CommandSuggestion.Enable_Color = false;
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.EnableColor = CommandSuggestion.Enable_Color;
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.Enable24bitColor = General.ConsoleColorMode == ConsoleColorModeType.vt100_24bit;
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.UseBasicArrow = CommandSuggestion.Use_Basic_Arrow;
|
||||
|
||||
CommandSuggestion.Max_Suggestion_Width =
|
||||
ConsoleInteractive.ConsoleSuggestion.SetMaxSuggestionLength(CommandSuggestion.Max_Suggestion_Width);
|
||||
|
||||
CommandSuggestion.Max_Displayed_Suggestions =
|
||||
ConsoleInteractive.ConsoleSuggestion.SetMaxSuggestionCount(CommandSuggestion.Max_Displayed_Suggestions);
|
||||
|
||||
// Suggestion color settings
|
||||
if (backend is ClassicConsoleBackend classic)
|
||||
{
|
||||
classic.EnableWriterColor = General.ConsoleColorMode != ConsoleColorModeType.disable;
|
||||
classic.UseVT100ColorCode = General.ConsoleColorMode != ConsoleColorModeType.legacy_4bit;
|
||||
|
||||
if (General.ConsoleColorMode == ConsoleColorModeType.disable)
|
||||
CommandSuggestion.Enable_Color = false;
|
||||
|
||||
classic.EnableSuggestionColor = CommandSuggestion.Enable_Color;
|
||||
classic.Enable24bitColor = General.ConsoleColorMode == ConsoleColorModeType.vt100_24bit;
|
||||
classic.UseBasicArrow = CommandSuggestion.Use_Basic_Arrow;
|
||||
|
||||
CommandSuggestion.Max_Suggestion_Width =
|
||||
classic.SetMaxSuggestionLength(CommandSuggestion.Max_Suggestion_Width);
|
||||
CommandSuggestion.Max_Displayed_Suggestions =
|
||||
classic.SetMaxSuggestionCount(CommandSuggestion.Max_Displayed_Suggestions);
|
||||
|
||||
if (!CheckColorCode(CommandSuggestion.Text_Color))
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Format(Translations.config_commandsuggestion_illegal_color, "CommandSuggestion.TextColor", CommandSuggestion.Text_Color));
|
||||
|
|
@ -1152,7 +1145,7 @@ namespace MinecraftClient
|
|||
CommandSuggestion.Arrow_Symbol_Color = "#d1d5db";
|
||||
}
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.SetColors(
|
||||
classic.SetSuggestionColors(
|
||||
CommandSuggestion.Text_Color, CommandSuggestion.Text_Background_Color,
|
||||
CommandSuggestion.Highlight_Text_Color, CommandSuggestion.Highlight_Text_Background_Color,
|
||||
CommandSuggestion.Tooltip_Color, CommandSuggestion.Highlight_Tooltip_Color,
|
||||
|
|
@ -1184,6 +1177,9 @@ namespace MinecraftClient
|
|||
[TomlDoNotInlineObject]
|
||||
public class MainConfig
|
||||
{
|
||||
[TomlInlineComment("$Console.General.ConsoleMode$")]
|
||||
public ConsoleModeType ConsoleMode = ConsoleModeType.classic;
|
||||
|
||||
[TomlInlineComment("$Console.General.ConsoleColorMode$")]
|
||||
public ConsoleColorModeType ConsoleColorMode = ConsoleColorModeType.vt100_24bit;
|
||||
|
||||
|
|
@ -1207,7 +1203,7 @@ namespace MinecraftClient
|
|||
|
||||
public int Max_Suggestion_Width = 30;
|
||||
|
||||
public int Max_Displayed_Suggestions = 6;
|
||||
public int Max_Displayed_Suggestions = 10;
|
||||
|
||||
public string Text_Color = "#f8fafc";
|
||||
|
||||
|
|
@ -1224,6 +1220,7 @@ namespace MinecraftClient
|
|||
public string Arrow_Symbol_Color = "#d1d5db";
|
||||
}
|
||||
|
||||
public enum ConsoleModeType { classic, tui };
|
||||
public enum ConsoleColorModeType { disable, legacy_4bit, vt100_4bit, vt100_8bit, vt100_24bit };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
MinecraftClient/Tui/InventoryApp.cs
Normal file
29
MinecraftClient/Tui/InventoryApp.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Consolonia.Themes;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class InventoryApp : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
Styles.Add(new ModernTheme());
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new Window
|
||||
{
|
||||
Content = new InventoryMainView(),
|
||||
Title = "MCC Inventory"
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
752
MinecraftClient/Tui/InventoryMainView.cs
Normal file
752
MinecraftClient/Tui/InventoryMainView.cs
Normal file
|
|
@ -0,0 +1,752 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class InventoryMainView : UserControl
|
||||
{
|
||||
private static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40));
|
||||
private static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55));
|
||||
private static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75));
|
||||
private static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90));
|
||||
private static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140));
|
||||
private static readonly IBrush BrName = Brushes.White;
|
||||
private static readonly IBrush BrCount = Brushes.Yellow;
|
||||
private static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80));
|
||||
private static readonly IBrush BrEquipLbl = Brushes.DarkCyan;
|
||||
private static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60));
|
||||
private static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80));
|
||||
private static readonly IBrush BrHeldItemBorder = Brushes.Yellow;
|
||||
|
||||
private int _slotW;
|
||||
private int _slotH;
|
||||
private int _nameMaxLen;
|
||||
private int _nameLines;
|
||||
private int _topGap;
|
||||
private int _termW;
|
||||
|
||||
private readonly InventoryViewModel _vm;
|
||||
private TextBlock _titleText = null!;
|
||||
private Border _infoDetailBorder = null!;
|
||||
private TextBlock _infoDetailText = null!;
|
||||
private TextBlock _cursorItemText = null!;
|
||||
private TextBlock _helpText = null!;
|
||||
|
||||
private TextBlock[] _hotbarIndicators = new TextBlock[9];
|
||||
private int _currentHotbarSlot = -1;
|
||||
|
||||
private Border? _lastHoveredSlotBorder;
|
||||
|
||||
private Canvas _overlayCanvas = null!;
|
||||
private Border _heldItemFloater = null!;
|
||||
private TextBlock _heldItemFloaterName = null!;
|
||||
private TextBlock _heldItemFloaterCount = null!;
|
||||
|
||||
private ScrollViewer _chatScrollViewer = null!;
|
||||
private ObservableCollection<string>? _chatLines;
|
||||
private int _lastTermW;
|
||||
private int _lastTermH;
|
||||
|
||||
public InventoryMainView()
|
||||
{
|
||||
var handler = InventoryTuiHost.ActiveHandler
|
||||
?? throw new InvalidOperationException("No active McClient");
|
||||
int windowId = InventoryTuiHost.ActiveWindowId;
|
||||
|
||||
_vm = new InventoryViewModel(handler, windowId);
|
||||
_currentHotbarSlot = handler.GetCurrentSlot();
|
||||
|
||||
_chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50)
|
||||
?? new ObservableCollection<string>();
|
||||
|
||||
RebuildUi();
|
||||
}
|
||||
|
||||
private void RebuildUi()
|
||||
{
|
||||
int termH;
|
||||
try
|
||||
{
|
||||
_termW = System.Console.WindowWidth;
|
||||
termH = System.Console.WindowHeight;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_termW = 120;
|
||||
termH = 40;
|
||||
}
|
||||
|
||||
_lastTermW = _termW;
|
||||
_lastTermH = termH;
|
||||
|
||||
int availW = _termW - 26;
|
||||
_slotW = Math.Clamp(availW / 9, 8, 18);
|
||||
_nameMaxLen = _slotW;
|
||||
|
||||
int topUsedW = _slotW * 4 + 8 + _slotW * 2 + 4 + _slotW;
|
||||
_topGap = Math.Max(2, (_slotW * 9 - topUsedW) / 2);
|
||||
|
||||
_slotH = Math.Clamp((termH - 8) / 6, 2, 5);
|
||||
_nameLines = _slotH;
|
||||
|
||||
_vm.SetSlotDisplayParams(_nameMaxLen, _nameLines);
|
||||
|
||||
_lastHoveredSlotBorder = null;
|
||||
|
||||
_titleText = new TextBlock
|
||||
{
|
||||
FontWeight = FontWeight.Bold,
|
||||
Foreground = Brushes.Cyan,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
_infoDetailText = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = Brushes.White,
|
||||
};
|
||||
|
||||
_infoDetailBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Padding = new Thickness(0),
|
||||
Child = _infoDetailText,
|
||||
};
|
||||
|
||||
_cursorItemText = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.Yellow,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
|
||||
_helpText = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
||||
Text = Translations.tui_inventory_controls_help,
|
||||
};
|
||||
|
||||
_heldItemFloaterName = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
_heldItemFloaterCount = new TextBlock
|
||||
{
|
||||
Foreground = BrCount,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
_heldItemFloater = new Border
|
||||
{
|
||||
Background = BrHeldItemBg,
|
||||
BorderBrush = BrHeldItemBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(1, 0),
|
||||
IsVisible = false,
|
||||
MaxWidth = 24,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children = { _heldItemFloaterName, _heldItemFloaterCount },
|
||||
},
|
||||
};
|
||||
|
||||
_overlayCanvas = new Canvas { IsHitTestVisible = false };
|
||||
_overlayCanvas.Children.Add(_heldItemFloater);
|
||||
|
||||
var chatLines = _chatLines!;
|
||||
chatLines.CollectionChanged += (_, _) =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var sv = _chatScrollViewer;
|
||||
if (sv.Extent.Height > sv.Viewport.Height)
|
||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||
}, Avalonia.Threading.DispatcherPriority.Background);
|
||||
};
|
||||
var chatItemsControl = new ItemsControl
|
||||
{
|
||||
ItemsSource = chatLines,
|
||||
Focusable = false,
|
||||
ItemTemplate = new FuncDataTemplate<string>((s, _) =>
|
||||
new TextBlock
|
||||
{
|
||||
Text = s,
|
||||
Foreground = Brushes.Gray,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
}),
|
||||
};
|
||||
_chatScrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = chatItemsControl,
|
||||
Background = Brushes.Black,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
||||
Padding = new Thickness(0),
|
||||
};
|
||||
|
||||
_hotbarIndicators = new TextBlock[9];
|
||||
|
||||
Content = BuildRootLayout();
|
||||
UpdateTitle();
|
||||
UpdateInfoPanel();
|
||||
|
||||
_chatScrollToBottom = true;
|
||||
_chatScrollViewer.ScrollChanged += OnChatScrollChanged;
|
||||
}
|
||||
|
||||
private bool _chatScrollToBottom = true;
|
||||
|
||||
private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (!_chatScrollToBottom) return;
|
||||
var sv = _chatScrollViewer;
|
||||
if (sv.Extent.Height > sv.Viewport.Height)
|
||||
{
|
||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||
_chatScrollToBottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Control BuildRootLayout()
|
||||
{
|
||||
// Layout (top-down):
|
||||
// Title
|
||||
// [InfoPanel(right)] [InventoryGrid(left)] <-- inventory area
|
||||
// ChatScrollViewer (full width, fills remaining)
|
||||
|
||||
var inventoryArea = BuildMainArea();
|
||||
DockPanel.SetDock(_titleText, Dock.Top);
|
||||
DockPanel.SetDock(inventoryArea, Dock.Top);
|
||||
|
||||
var mainContent = new DockPanel
|
||||
{
|
||||
Children = { _titleText, inventoryArea, _chatScrollViewer }
|
||||
};
|
||||
|
||||
return new Panel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children = { mainContent, _overlayCanvas }
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildMainArea()
|
||||
{
|
||||
var infoPanel = BuildInfoPanel();
|
||||
DockPanel.SetDock(infoPanel, Dock.Right);
|
||||
|
||||
return new DockPanel
|
||||
{
|
||||
Children = { infoPanel, BuildInventoryPanel() }
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildInfoPanel()
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = Brushes.Gray,
|
||||
Padding = new Thickness(1),
|
||||
Width = 24,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan },
|
||||
_infoDetailBorder,
|
||||
new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) },
|
||||
_cursorItemText,
|
||||
new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) },
|
||||
_helpText,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildInventoryPanel()
|
||||
{
|
||||
var root = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
root.Children.Add(BuildTopSection());
|
||||
root.Children.Add(new Border { Height = 1 });
|
||||
root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9));
|
||||
root.Children.Add(BuildHotbarSection());
|
||||
|
||||
return new Border
|
||||
{
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = Brushes.Gray,
|
||||
Child = root,
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildTopSection()
|
||||
{
|
||||
var row = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
var offPanel = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 1, 0),
|
||||
};
|
||||
offPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_inventory_offhand,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
offPanel.Children.Add(CreateSlotCell(_vm.OffhandSlot, 0, 0));
|
||||
row.Children.Add(offPanel);
|
||||
|
||||
var equipGrid = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions("Auto,Auto"),
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto,Auto"),
|
||||
};
|
||||
|
||||
void AddEquipSlot(int r, int gc, string label, int eqIdx)
|
||||
{
|
||||
var lbl = MakeLabel(label);
|
||||
Grid.SetRow(lbl, r); Grid.SetColumn(lbl, gc);
|
||||
equipGrid.Children.Add(lbl);
|
||||
var btn = CreateSlotCell(_vm.EquipmentSlots[eqIdx], r, gc / 2);
|
||||
Grid.SetRow(btn, r); Grid.SetColumn(btn, gc + 1);
|
||||
equipGrid.Children.Add(btn);
|
||||
}
|
||||
|
||||
AddEquipSlot(0, 0, Translations.tui_inventory_equip_head, 0);
|
||||
AddEquipSlot(0, 2, Translations.tui_inventory_equip_body, 1);
|
||||
AddEquipSlot(1, 0, Translations.tui_inventory_equip_legs, 2);
|
||||
AddEquipSlot(1, 2, Translations.tui_inventory_equip_feet, 3);
|
||||
|
||||
row.Children.Add(equipGrid);
|
||||
row.Children.Add(new Border { Width = _topGap });
|
||||
|
||||
var craftGrid = new Grid
|
||||
{
|
||||
RowDefinitions = new RowDefinitions("Auto,Auto"),
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto,Auto"),
|
||||
};
|
||||
|
||||
for (int ci = 0; ci < 4; ci++)
|
||||
{
|
||||
int cr = ci / 2, cc = ci % 2;
|
||||
var cs = CreateSlotCell(_vm.CraftingInputSlots[ci], cr, cc);
|
||||
Grid.SetRow(cs, cr);
|
||||
Grid.SetColumn(cs, cc);
|
||||
craftGrid.Children.Add(cs);
|
||||
}
|
||||
|
||||
var arrowTb = new TextBlock
|
||||
{
|
||||
Text = "=>",
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Padding = new Thickness(1, 0),
|
||||
};
|
||||
Grid.SetRow(arrowTb, 1); Grid.SetColumn(arrowTb, 2);
|
||||
craftGrid.Children.Add(arrowTb);
|
||||
|
||||
var craftOutPanel = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
craftOutPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_inventory_output,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
craftOutPanel.Children.Add(CreateSlotCell(_vm.CraftingOutputSlot, 0, 1));
|
||||
Grid.SetRow(craftOutPanel, 0); Grid.SetColumn(craftOutPanel, 3);
|
||||
Grid.SetRowSpan(craftOutPanel, 2);
|
||||
craftGrid.Children.Add(craftOutPanel);
|
||||
|
||||
row.Children.Add(craftGrid);
|
||||
return row;
|
||||
}
|
||||
|
||||
private Control BuildHotbarSection()
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 0 };
|
||||
|
||||
var numberRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bool active = i == _currentHotbarSlot;
|
||||
string label = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||
|
||||
var tb = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Width = _slotW,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
_hotbarIndicators[i] = tb;
|
||||
numberRow.Children.Add(tb);
|
||||
}
|
||||
panel.Children.Add(numberRow);
|
||||
panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9));
|
||||
return panel;
|
||||
}
|
||||
|
||||
private TextBlock MakeLabel(string text)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
Foreground = BrEquipLbl,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(1, 0, 0, 0),
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildSlotGrid(ObservableCollection<SlotViewModel> slots, int columns)
|
||||
{
|
||||
var grid = new Grid();
|
||||
int rows = (slots.Count + columns - 1) / columns;
|
||||
|
||||
for (int r = 0; r < rows; r++)
|
||||
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
for (int c = 0; c < columns; c++)
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||
|
||||
for (int i = 0; i < slots.Count; i++)
|
||||
{
|
||||
int row = i / columns;
|
||||
int col = i % columns;
|
||||
var cell = CreateSlotCell(slots[i], row, col);
|
||||
Grid.SetRow(cell, row);
|
||||
Grid.SetColumn(cell, col);
|
||||
grid.Children.Add(cell);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private static IBrush GetSlotBg(bool isEmpty, int row, int col)
|
||||
{
|
||||
bool isA = (row + col) % 2 == 0;
|
||||
return isEmpty
|
||||
? (isA ? BrSlotEmptyA : BrSlotEmptyB)
|
||||
: (isA ? BrSlotFillA : BrSlotFillB);
|
||||
}
|
||||
|
||||
private Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0)
|
||||
{
|
||||
var nameTb = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
};
|
||||
|
||||
var countTb = new TextBlock
|
||||
{
|
||||
Foreground = BrCount,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
};
|
||||
|
||||
ApplySlotVisual(slot, nameTb, countTb);
|
||||
|
||||
int r = row, c = col;
|
||||
var border = new Border
|
||||
{
|
||||
Width = _slotW,
|
||||
Height = _slotH,
|
||||
Background = GetSlotBg(slot.IsEmpty, r, c),
|
||||
Child = new Panel
|
||||
{
|
||||
Children = { nameTb, countTb },
|
||||
},
|
||||
Tag = (slot, r, c),
|
||||
};
|
||||
|
||||
border.PointerPressed += OnSlotPointerPressed;
|
||||
border.PointerEntered += OnSlotPointerEnter;
|
||||
border.PointerExited += OnSlotPointerExit;
|
||||
border.PointerMoved += OnSlotPointerMoved;
|
||||
|
||||
slot.PropertyChanged += (_, _) =>
|
||||
{
|
||||
ApplySlotVisual(slot, nameTb, countTb);
|
||||
border.Background = GetSlotBg(slot.IsEmpty, r, c);
|
||||
};
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
private void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb)
|
||||
{
|
||||
if (slot.IsEmpty)
|
||||
{
|
||||
nameTb.Text = "";
|
||||
nameTb.Foreground = BrDim;
|
||||
countTb.Text = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
nameTb.Text = slot.ItemDisplayText;
|
||||
nameTb.Foreground = BrName;
|
||||
countTb.Text = slot.CountDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int))
|
||||
return;
|
||||
|
||||
SetHover(border, slot);
|
||||
|
||||
var point = e.GetCurrentPoint(border);
|
||||
bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0;
|
||||
|
||||
WindowActionType action;
|
||||
if (point.Properties.IsRightButtonPressed)
|
||||
action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick;
|
||||
else
|
||||
action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick;
|
||||
|
||||
_vm.PerformAction(slot.SlotId, action);
|
||||
UpdateInfoPanel();
|
||||
UpdateHeldItemFloater(e);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSlotPointerEnter(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||
{
|
||||
SetHover(b, slot);
|
||||
UpdateHeldItemFloater(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||
{
|
||||
SetHover(b, slot);
|
||||
UpdateHeldItemFloater(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerExit(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col))
|
||||
b.Background = GetSlotBg(slot.IsEmpty, row, col);
|
||||
}
|
||||
|
||||
private void SetHover(Border border, SlotViewModel slot)
|
||||
{
|
||||
if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border)
|
||||
{
|
||||
if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc))
|
||||
_lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc);
|
||||
}
|
||||
|
||||
_lastHoveredSlotBorder = border;
|
||||
border.Background = BrSlotHover;
|
||||
_vm.HoveredSlot = slot;
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
|
||||
private void UpdateHeldItemFloater(PointerEventArgs e)
|
||||
{
|
||||
if (!_vm.HasCursorItem)
|
||||
{
|
||||
_heldItemFloater.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_heldItemFloaterName.Text = _vm.CursorItemInfo;
|
||||
_heldItemFloaterCount.Text = "";
|
||||
|
||||
try
|
||||
{
|
||||
var pos = e.GetPosition(_overlayCanvas);
|
||||
double left = pos.X + 2;
|
||||
double remainingW = _termW - left - 2;
|
||||
int maxW = Math.Max(8, (int)remainingW);
|
||||
_heldItemFloater.MaxWidth = maxW;
|
||||
Canvas.SetLeft(_heldItemFloater, left);
|
||||
Canvas.SetTop(_heldItemFloater, pos.Y);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_heldItemFloater.MaxWidth = 24;
|
||||
Canvas.SetLeft(_heldItemFloater, 0);
|
||||
Canvas.SetTop(_heldItemFloater, 0);
|
||||
}
|
||||
|
||||
_heldItemFloater.IsVisible = true;
|
||||
}
|
||||
|
||||
private void UpdateInfoPanel()
|
||||
{
|
||||
_infoDetailText.Text = _vm.HoveredSlotDetailText;
|
||||
|
||||
bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty;
|
||||
_infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent;
|
||||
|
||||
if (_vm.HasCursorItem)
|
||||
{
|
||||
_cursorItemText.Text = _vm.CursorItemInfo;
|
||||
_cursorItemText.Foreground = Brushes.Yellow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cursorItemText.Text = Translations.tui_inventory_cursor_empty;
|
||||
_cursorItemText.Foreground = BrDim;
|
||||
_heldItemFloater.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTitle()
|
||||
{
|
||||
_titleText.Text = _vm.Title;
|
||||
}
|
||||
|
||||
private void CloseInventory()
|
||||
{
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend)
|
||||
tuiBackend.GetView()?.HideOverlay();
|
||||
else
|
||||
(Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
case Key.E:
|
||||
CloseInventory();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.C:
|
||||
if ((e.KeyModifiers & KeyModifiers.Shift) != 0 &&
|
||||
_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||
{
|
||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick);
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Q:
|
||||
if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||
{
|
||||
var action = (e.KeyModifiers & KeyModifiers.Control) != 0
|
||||
? WindowActionType.DropItemStack
|
||||
: WindowActionType.DropItem;
|
||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, action);
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.R:
|
||||
_vm.RefreshFromContainer();
|
||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||
UpdateHotbarIndicators();
|
||||
UpdateInfoPanel();
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHotbarIndicators()
|
||||
{
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bool active = i == _currentHotbarSlot;
|
||||
_hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||
_hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
Focusable = true;
|
||||
Focus();
|
||||
AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
||||
SizeChanged += OnViewSizeChanged;
|
||||
}
|
||||
|
||||
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
CloseInventory();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
int newW, newH;
|
||||
try
|
||||
{
|
||||
newW = System.Console.WindowWidth;
|
||||
newH = System.Console.WindowHeight;
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (newW == _lastTermW && newH == _lastTermH) return;
|
||||
|
||||
_vm.RefreshFromContainer();
|
||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||
RebuildUi();
|
||||
Focus();
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(GotFocusEventArgs e)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
Focusable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
182
MinecraftClient/Tui/InventoryTuiHost.cs
Normal file
182
MinecraftClient/Tui/InventoryTuiHost.cs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
using Consolonia;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the lifecycle of the inventory TUI.
|
||||
/// In TUI mode: opens as a Consolonia dialog window.
|
||||
/// In classic mode: launches standalone Consolonia on a dedicated thread.
|
||||
/// </summary>
|
||||
public static class InventoryTuiHost
|
||||
{
|
||||
private static volatile bool _isRunning;
|
||||
private static bool _classicEverLaunched;
|
||||
|
||||
public static McClient? ActiveHandler { get; private set; }
|
||||
public static int ActiveWindowId { get; private set; }
|
||||
|
||||
public static bool IsRunning => _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the TUI can be launched (classic mode has a one-shot limit).
|
||||
/// </summary>
|
||||
public static bool CanLaunch
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isRunning) return false;
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend) return true;
|
||||
return !_classicEverLaunched;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before standalone TUI takes over the terminal (classic mode only).
|
||||
/// </summary>
|
||||
public static Action? OnSuspendConsole { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called after standalone TUI releases the terminal (classic mode only).
|
||||
/// </summary>
|
||||
public static Action? OnResumeConsole { get; set; }
|
||||
|
||||
public static bool Launch(McClient handler, int windowId)
|
||||
{
|
||||
if (_isRunning)
|
||||
return false;
|
||||
|
||||
Container? container = handler.GetInventory(windowId);
|
||||
if (container == null)
|
||||
return false;
|
||||
|
||||
_isRunning = true;
|
||||
ActiveHandler = handler;
|
||||
ActiveWindowId = windowId;
|
||||
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend)
|
||||
{
|
||||
LaunchAsDialog();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_classicEverLaunched)
|
||||
{
|
||||
_isRunning = false;
|
||||
ActiveHandler = null;
|
||||
return false;
|
||||
}
|
||||
var tuiThread = new Thread(RunClassicTui) { Name = "InventoryTUI", IsBackground = false };
|
||||
tuiThread.Start();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open inventory as an overlay panel within the main TUI view.
|
||||
/// </summary>
|
||||
private static void LaunchAsDialog()
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var view = TuiConsoleBackend.Instance?.GetView();
|
||||
if (view != null)
|
||||
{
|
||||
var content = new InventoryMainView();
|
||||
view.ShowOverlay(content, () =>
|
||||
{
|
||||
ActiveHandler = null;
|
||||
_isRunning = false;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveHandler = null;
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted($"§c[InventoryTUI] Error: {ex.Message}");
|
||||
ConsoleIO.WriteLineFormatted($"§c[InventoryTUI] Stack: {ex.StackTrace}");
|
||||
if (ex.InnerException != null)
|
||||
ConsoleIO.WriteLineFormatted($"§c[InventoryTUI] Inner: {ex.InnerException.Message}");
|
||||
ActiveHandler = null;
|
||||
_isRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classic mode: run standalone Consolonia on a dedicated thread.
|
||||
/// </summary>
|
||||
private static void RunClassicTui()
|
||||
{
|
||||
try
|
||||
{
|
||||
OnSuspendConsole?.Invoke();
|
||||
_classicEverLaunched = true;
|
||||
|
||||
AppBuilder builder = AppBuilder.Configure<InventoryApp>()
|
||||
.UseConsolonia()
|
||||
.UseAutoDetectedConsole()
|
||||
.LogToException();
|
||||
|
||||
builder.StartWithConsoleLifetime(Array.Empty<string>());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Console.Error.WriteLine($"[InventoryTUI] Error: {ex.Message}");
|
||||
System.Console.Error.WriteLine($"[InventoryTUI] Stack: {ex.StackTrace}");
|
||||
if (ex.InnerException != null)
|
||||
System.Console.Error.WriteLine($"[InventoryTUI] Inner: {ex.InnerException}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreTerminalState();
|
||||
OnResumeConsole?.Invoke();
|
||||
ActiveHandler = null;
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestoreTerminalState()
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Console.Write("\x1b[?1049l");
|
||||
System.Console.Write("\x1b[?25h");
|
||||
System.Console.Write("\x1b[?1000l");
|
||||
System.Console.Write("\x1b[?1002l");
|
||||
System.Console.Write("\x1b[?1003l");
|
||||
System.Console.Write("\x1b[?1006l");
|
||||
System.Console.Write("\x1b[?2004l");
|
||||
System.Console.Write("\x1b[0m");
|
||||
System.Console.Write("\x1b(B");
|
||||
System.Console.Out.Flush();
|
||||
|
||||
try
|
||||
{
|
||||
using var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "stty",
|
||||
Arguments = "sane",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
});
|
||||
proc?.WaitForExit(2000);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
220
MinecraftClient/Tui/InventoryViewModel.cs
Normal file
220
MinecraftClient/Tui/InventoryViewModel.cs
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class InventoryViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private SlotViewModel? _hoveredSlot;
|
||||
private string _title = "";
|
||||
private string _statusText = "";
|
||||
private string _cursorItemInfo = "";
|
||||
private bool _hasCursorItem;
|
||||
|
||||
public McClient Handler { get; }
|
||||
public int WindowId { get; }
|
||||
|
||||
public ObservableCollection<SlotViewModel> EquipmentSlots { get; } = new();
|
||||
public ObservableCollection<SlotViewModel> CraftingInputSlots { get; } = new();
|
||||
public SlotViewModel CraftingOutputSlot { get; }
|
||||
public ObservableCollection<SlotViewModel> MainInventorySlots { get; } = new();
|
||||
public ObservableCollection<SlotViewModel> HotbarSlots { get; } = new();
|
||||
public SlotViewModel OffhandSlot { get; }
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set { _title = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set { _statusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string CursorItemInfo
|
||||
{
|
||||
get => _cursorItemInfo;
|
||||
set { _cursorItemInfo = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool HasCursorItem
|
||||
{
|
||||
get => _hasCursorItem;
|
||||
set { _hasCursorItem = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public SlotViewModel? HoveredSlot
|
||||
{
|
||||
get => _hoveredSlot;
|
||||
set
|
||||
{
|
||||
if (_hoveredSlot != null)
|
||||
_hoveredSlot.IsHovered = false;
|
||||
_hoveredSlot = value;
|
||||
if (_hoveredSlot != null)
|
||||
_hoveredSlot.IsHovered = true;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multi-line detail text for the hovered slot.
|
||||
/// </summary>
|
||||
public string HoveredSlotDetailText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hoveredSlot == null)
|
||||
return Translations.tui_inventory_hover_hint;
|
||||
|
||||
if (_hoveredSlot.IsEmpty)
|
||||
return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(_hoveredSlot.ItemTypeName);
|
||||
sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount));
|
||||
|
||||
string fullInfo = _hoveredSlot.FullInfo;
|
||||
if (!string.IsNullOrEmpty(fullInfo))
|
||||
{
|
||||
string[] parts = fullInfo.Split(" | ");
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
sb.AppendLine(parts[i].Trim());
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<int, SlotViewModel> _slotMap = new();
|
||||
private int _nameMaxLen = 9;
|
||||
private int _nameMaxLines = 1;
|
||||
|
||||
public InventoryViewModel(McClient handler, int windowId)
|
||||
{
|
||||
Handler = handler;
|
||||
WindowId = windowId;
|
||||
|
||||
CraftingOutputSlot = new SlotViewModel(0);
|
||||
OffhandSlot = new SlotViewModel(45);
|
||||
|
||||
InitializeSlots();
|
||||
RefreshFromContainer();
|
||||
}
|
||||
|
||||
public void SetSlotDisplayParams(int maxWidth, int maxLines)
|
||||
{
|
||||
_nameMaxLen = maxWidth;
|
||||
_nameMaxLines = maxLines;
|
||||
foreach (var kvp in _slotMap)
|
||||
{
|
||||
kvp.Value.NameMaxWidth = maxWidth;
|
||||
kvp.Value.NameMaxLines = maxLines;
|
||||
}
|
||||
RefreshFromContainer();
|
||||
}
|
||||
|
||||
private void InitializeSlots()
|
||||
{
|
||||
_slotMap.Clear();
|
||||
|
||||
_slotMap[0] = CraftingOutputSlot;
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
CraftingInputSlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 5; i <= 8; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
EquipmentSlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 9; i <= 35; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 36; i <= 44; i++)
|
||||
{
|
||||
int hotbarIdx = i - 36;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
}
|
||||
|
||||
_slotMap[45] = OffhandSlot;
|
||||
}
|
||||
|
||||
public void RefreshFromContainer()
|
||||
{
|
||||
Inventory.Container? container = Handler.GetInventory(WindowId);
|
||||
if (container == null)
|
||||
{
|
||||
StatusText = Translations.tui_inventory_container_not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title);
|
||||
|
||||
foreach (var kvp in _slotMap)
|
||||
{
|
||||
Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null;
|
||||
kvp.Value.Update(item);
|
||||
}
|
||||
|
||||
UpdateCursorItem(container);
|
||||
int itemCount = 0;
|
||||
foreach (var kvp in container.Items)
|
||||
{
|
||||
if (kvp.Key >= 0 && !kvp.Value.IsEmpty)
|
||||
itemCount++;
|
||||
}
|
||||
StatusText = string.Format(Translations.tui_inventory_item_count, itemCount);
|
||||
|
||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||
}
|
||||
|
||||
private void UpdateCursorItem(Inventory.Container container)
|
||||
{
|
||||
if (container.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty)
|
||||
{
|
||||
CursorItemInfo = $"x{cursorItem.Count} {cursorItem.GetTypeString()}";
|
||||
HasCursorItem = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CursorItemInfo = "";
|
||||
HasCursorItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool PerformAction(int slotId, WindowActionType action)
|
||||
{
|
||||
bool result = Handler.DoWindowAction(WindowId, slotId, action);
|
||||
RefreshFromContainer();
|
||||
return result;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
932
MinecraftClient/Tui/MainTuiView.cs
Normal file
932
MinecraftClient/Tui/MainTuiView.cs
Normal file
|
|
@ -0,0 +1,932 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class MainTuiView : UserControl
|
||||
{
|
||||
private const int MaxLogLines = 5000;
|
||||
private const int CtrlCDoublePressMsec = 1500;
|
||||
|
||||
private readonly ObservableCollection<string> _logLines = new();
|
||||
private readonly ObservableCollection<Control> _logControls = new();
|
||||
private readonly ItemsControl _logItemsControl;
|
||||
private readonly ScrollViewer _logScrollViewer;
|
||||
private readonly TextBox _commandInput;
|
||||
private bool _autoScroll = true;
|
||||
private bool _programmaticScroll;
|
||||
private readonly ObservableCollection<string> _commandHistory = new();
|
||||
private int _historyIndex = -1;
|
||||
|
||||
private readonly Panel _rootPanel;
|
||||
private readonly DockPanel _mainContent;
|
||||
private Control? _overlayContent;
|
||||
private Action? _overlayCloseCallback;
|
||||
|
||||
private readonly TextBlock _statusBar;
|
||||
private readonly Border _notificationBorder;
|
||||
private readonly TextBlock _notificationText;
|
||||
private long _lastCtrlCTicks;
|
||||
private long _lastLogClickTicks;
|
||||
private const int DoubleClickMsec = 500;
|
||||
|
||||
private readonly Border _suggestionBorder;
|
||||
private readonly StackPanel _suggestionPanel;
|
||||
private CommandSuggestion[] _suggestions = Array.Empty<CommandSuggestion>();
|
||||
private (int Start, int End) _suggestionRange;
|
||||
private int _selectedSuggestionIndex = -1;
|
||||
private int _suggestionViewTop;
|
||||
private bool _acceptingSuggestion;
|
||||
private bool _tabCycling;
|
||||
|
||||
private int MaxVisibleSuggestions =>
|
||||
Math.Max(1, Settings.Config.Console.CommandSuggestion.Max_Displayed_Suggestions);
|
||||
|
||||
public MainTuiView()
|
||||
{
|
||||
Background = Brushes.Black;
|
||||
|
||||
_statusBar = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.Gray,
|
||||
Background = Brushes.Black,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
IsVisible = false,
|
||||
};
|
||||
|
||||
_logItemsControl = new ItemsControl
|
||||
{
|
||||
ItemsSource = _logControls,
|
||||
Focusable = false,
|
||||
};
|
||||
|
||||
_logScrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = _logItemsControl,
|
||||
Background = Brushes.Black,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Padding = new Thickness(0),
|
||||
Focusable = false,
|
||||
};
|
||||
|
||||
_logScrollViewer.ScrollChanged += OnLogScrollChanged;
|
||||
_logScrollViewer.PointerPressed += OnLogAreaPointerPressed;
|
||||
|
||||
_commandInput = new TextBox
|
||||
{
|
||||
Watermark = "",
|
||||
Foreground = Brushes.White,
|
||||
Background = Brushes.Black,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
MinHeight = 1,
|
||||
};
|
||||
|
||||
_commandInput.AddHandler(KeyDownEvent, OnCommandKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
||||
_commandInput.TextChanged += OnCommandTextChanged;
|
||||
|
||||
var promptLabel = new TextBlock
|
||||
{
|
||||
Text = "> ",
|
||||
Foreground = Brushes.Cyan,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
|
||||
var inputRow = new DockPanel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children =
|
||||
{
|
||||
SetDock(promptLabel, Dock.Left),
|
||||
_commandInput
|
||||
}
|
||||
};
|
||||
|
||||
_notificationText = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.Yellow,
|
||||
Padding = new Thickness(1, 0),
|
||||
};
|
||||
_notificationBorder = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromRgb(60, 50, 20)),
|
||||
BorderBrush = Brushes.Yellow,
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = _notificationText,
|
||||
IsVisible = false,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
};
|
||||
|
||||
_suggestionPanel = new StackPanel
|
||||
{
|
||||
Orientation = Avalonia.Layout.Orientation.Vertical,
|
||||
};
|
||||
_suggestionPanel.PointerWheelChanged += OnSuggestionWheelChanged;
|
||||
_suggestionBorder = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromRgb(30, 30, 30)),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = _suggestionPanel,
|
||||
IsVisible = false,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
Margin = new Thickness(0, 0, 0, 1),
|
||||
};
|
||||
|
||||
_mainContent = new DockPanel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children =
|
||||
{
|
||||
SetDock(_statusBar, Dock.Top),
|
||||
SetDock(inputRow, Dock.Bottom),
|
||||
_logScrollViewer
|
||||
}
|
||||
};
|
||||
|
||||
_rootPanel = new Panel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children = { _mainContent, _notificationBorder, _suggestionBorder }
|
||||
};
|
||||
|
||||
Content = _rootPanel;
|
||||
|
||||
StartStatusBarTimer();
|
||||
}
|
||||
|
||||
private static Control SetDock(Control control, Dock dock)
|
||||
{
|
||||
DockPanel.SetDock(control, dock);
|
||||
return control;
|
||||
}
|
||||
|
||||
#region Log output
|
||||
|
||||
public void AppendLogLine(string text)
|
||||
{
|
||||
_logLines.Add(text);
|
||||
|
||||
var tb = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
Foreground = Brushes.White,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
_logControls.Add(tb);
|
||||
|
||||
TrimLog();
|
||||
|
||||
if (_autoScroll)
|
||||
ScheduleScrollToEnd();
|
||||
}
|
||||
|
||||
public void AppendFormattedLogLine(string text)
|
||||
{
|
||||
_logLines.Add(text);
|
||||
|
||||
var tb = McColorParser.CreateColoredTextBlock(text, TextWrapping.Wrap);
|
||||
_logControls.Add(tb);
|
||||
|
||||
TrimLog();
|
||||
|
||||
if (_autoScroll)
|
||||
ScheduleScrollToEnd();
|
||||
}
|
||||
|
||||
private void TrimLog()
|
||||
{
|
||||
while (_logLines.Count > MaxLogLines)
|
||||
{
|
||||
_logLines.RemoveAt(0);
|
||||
_logControls.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleScrollToEnd()
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_programmaticScroll = true;
|
||||
var sv = _logScrollViewer;
|
||||
sv.Offset = new Vector(0, sv.Extent.Height);
|
||||
_programmaticScroll = false;
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
public string LatestLogLine => _logLines.Count > 0 ? _logLines[^1] : "";
|
||||
|
||||
public ObservableCollection<string> GetRecentLogLines(int _) => _logLines;
|
||||
|
||||
private void OnLogAreaPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var props = e.GetCurrentPoint(null).Properties;
|
||||
if (!props.IsLeftButtonPressed)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => _commandInput.Focus());
|
||||
return;
|
||||
}
|
||||
|
||||
bool shift = (e.KeyModifiers & KeyModifiers.Shift) != 0;
|
||||
if (shift)
|
||||
return;
|
||||
|
||||
long now = Environment.TickCount64;
|
||||
long elapsed = now - _lastLogClickTicks;
|
||||
_lastLogClickTicks = now;
|
||||
|
||||
if (elapsed < DoubleClickMsec)
|
||||
{
|
||||
ShowNotification(Translations.tui_select_copy_hint, 3000);
|
||||
_lastLogClickTicks = 0;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(() => _commandInput.Focus());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Input
|
||||
|
||||
public void ClearInput()
|
||||
{
|
||||
_commandInput.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void OnCommandKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (_tabCycling && e.Key is not (Key.Tab or Key.Up or Key.Down or Key.Escape))
|
||||
_tabCycling = false;
|
||||
|
||||
bool ctrl = (e.KeyModifiers & KeyModifiers.Control) != 0;
|
||||
|
||||
if (e.Key == Key.C && ctrl)
|
||||
{
|
||||
HandleCtrlC();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.Key == Key.Back || e.Key == Key.W) && ctrl)
|
||||
{
|
||||
DeleteWordBackward();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Left && ctrl)
|
||||
{
|
||||
MoveCaretWordLeft();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Right && ctrl)
|
||||
{
|
||||
MoveCaretWordRight();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.A && ctrl)
|
||||
{
|
||||
_commandInput.CaretIndex = 0;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.E && ctrl)
|
||||
{
|
||||
_commandInput.CaretIndex = _commandInput.Text?.Length ?? 0;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.U && ctrl)
|
||||
{
|
||||
_commandInput.Text = string.Empty;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Escape && SuggestionsVisible)
|
||||
{
|
||||
ClearSuggestions();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Tab && SuggestionsVisible)
|
||||
{
|
||||
if (_tabCycling)
|
||||
{
|
||||
MoveSuggestionSelection(1);
|
||||
ApplySuggestionInPlace(_selectedSuggestionIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplySuggestionInPlace(_selectedSuggestionIndex);
|
||||
_tabCycling = true;
|
||||
}
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Key == Key.Tab)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Enter:
|
||||
SubmitCommand();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Up:
|
||||
if (SuggestionsVisible)
|
||||
MoveSuggestionSelection(-1);
|
||||
else
|
||||
NavigateHistory(-1);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Down:
|
||||
if (SuggestionsVisible)
|
||||
MoveSuggestionSelection(1);
|
||||
else
|
||||
NavigateHistory(1);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.PageUp:
|
||||
ScrollLog(-10);
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.PageDown:
|
||||
ScrollLog(10);
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteWordBackward()
|
||||
{
|
||||
string text = _commandInput.Text ?? "";
|
||||
int caret = _commandInput.CaretIndex;
|
||||
if (caret == 0 || text.Length == 0) return;
|
||||
|
||||
int pos = caret - 1;
|
||||
while (pos > 0 && text[pos - 1] == ' ') pos--;
|
||||
while (pos > 0 && text[pos - 1] != ' ') pos--;
|
||||
|
||||
_commandInput.Text = text[..pos] + text[caret..];
|
||||
_commandInput.CaretIndex = pos;
|
||||
}
|
||||
|
||||
private void MoveCaretWordLeft()
|
||||
{
|
||||
string text = _commandInput.Text ?? "";
|
||||
int pos = _commandInput.CaretIndex;
|
||||
if (pos == 0) return;
|
||||
|
||||
pos--;
|
||||
while (pos > 0 && text[pos - 1] == ' ') pos--;
|
||||
while (pos > 0 && text[pos - 1] != ' ') pos--;
|
||||
|
||||
_commandInput.CaretIndex = pos;
|
||||
}
|
||||
|
||||
private void MoveCaretWordRight()
|
||||
{
|
||||
string text = _commandInput.Text ?? "";
|
||||
int pos = _commandInput.CaretIndex;
|
||||
if (pos >= text.Length) return;
|
||||
|
||||
while (pos < text.Length && text[pos] != ' ') pos++;
|
||||
while (pos < text.Length && text[pos] == ' ') pos++;
|
||||
|
||||
_commandInput.CaretIndex = pos;
|
||||
}
|
||||
|
||||
private void OnCommandTextChanged(object? sender, TextChangedEventArgs e)
|
||||
{
|
||||
string text = _commandInput.Text ?? string.Empty;
|
||||
|
||||
if (text.Contains('\n') || text.Contains('\r'))
|
||||
{
|
||||
string cleaned = text.Replace("\r\n", " ").Replace('\r', ' ').Replace('\n', ' ');
|
||||
_commandInput.Text = cleaned;
|
||||
_commandInput.CaretIndex = cleaned.Length;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_acceptingSuggestion || _tabCycling)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
ClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
var backend = TuiConsoleBackend.Instance;
|
||||
if (backend == null) return;
|
||||
int cursor = _commandInput.CaretIndex;
|
||||
backend.OnInputChanged(text, cursor);
|
||||
}
|
||||
|
||||
private void SubmitCommand()
|
||||
{
|
||||
string command = _commandInput.Text?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(command))
|
||||
return;
|
||||
|
||||
ClearSuggestions();
|
||||
_tabCycling = false;
|
||||
|
||||
_commandHistory.Add(command);
|
||||
_historyIndex = _commandHistory.Count;
|
||||
|
||||
_acceptingSuggestion = true;
|
||||
try { _commandInput.Text = string.Empty; }
|
||||
finally { _acceptingSuggestion = false; }
|
||||
|
||||
_autoScroll = true;
|
||||
|
||||
AppendLogLine($"> {command}");
|
||||
|
||||
TuiConsoleBackend.Instance?.OnCommandSubmitted(command);
|
||||
}
|
||||
|
||||
private void NavigateHistory(int direction)
|
||||
{
|
||||
if (_commandHistory.Count == 0)
|
||||
return;
|
||||
|
||||
_historyIndex += direction;
|
||||
if (_historyIndex < 0) _historyIndex = 0;
|
||||
if (_historyIndex >= _commandHistory.Count)
|
||||
{
|
||||
_historyIndex = _commandHistory.Count;
|
||||
_commandInput.Text = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
string historyText = _commandHistory[_historyIndex];
|
||||
_commandInput.Text = historyText;
|
||||
_commandInput.CaretIndex = historyText.Length;
|
||||
Dispatcher.UIThread.Post(() => _commandInput.CaretIndex = historyText.Length,
|
||||
DispatcherPriority.Input);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Suggestions
|
||||
|
||||
private const int PromptWidth = 2; // "> "
|
||||
private const int BorderAndPadding = 2; // 1 border + 1 padding on each side
|
||||
|
||||
internal void UpdateSuggestions(CommandSuggestion[] suggestions, (int Start, int End) range)
|
||||
{
|
||||
if (suggestions.Length == 0)
|
||||
{
|
||||
ClearSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
_suggestions = suggestions;
|
||||
_suggestionRange = range;
|
||||
_selectedSuggestionIndex = 0;
|
||||
_suggestionViewTop = 0;
|
||||
|
||||
int leftOffset = PromptWidth + range.Start - BorderAndPadding;
|
||||
double screenWidth = Bounds.Width;
|
||||
if (screenWidth < 1)
|
||||
screenWidth = 80;
|
||||
|
||||
if (leftOffset < 0)
|
||||
leftOffset = 0;
|
||||
|
||||
_suggestionBorder.Margin = new Thickness(leftOffset, 0, 0, 1);
|
||||
_suggestionBorder.MaxWidth = Math.Max(10, screenWidth - leftOffset);
|
||||
|
||||
RebuildSuggestionItems();
|
||||
_suggestionBorder.IsVisible = true;
|
||||
}
|
||||
|
||||
internal void ClearSuggestions()
|
||||
{
|
||||
if (!_suggestionBorder.IsVisible && _suggestions.Length == 0)
|
||||
return;
|
||||
|
||||
_suggestions = Array.Empty<CommandSuggestion>();
|
||||
_selectedSuggestionIndex = -1;
|
||||
_suggestionBorder.IsVisible = false;
|
||||
_suggestionPanel.Children.Clear();
|
||||
}
|
||||
|
||||
private bool SuggestionsVisible => _suggestionBorder.IsVisible && _suggestions.Length > 0;
|
||||
|
||||
private void RebuildSuggestionItems()
|
||||
{
|
||||
_suggestionPanel.Children.Clear();
|
||||
|
||||
int visibleCount = Math.Min(_suggestions.Length, MaxVisibleSuggestions);
|
||||
int viewBottom = _suggestionViewTop + visibleCount;
|
||||
|
||||
for (int i = _suggestionViewTop; i < viewBottom && i < _suggestions.Length; i++)
|
||||
{
|
||||
var sug = _suggestions[i];
|
||||
int index = i;
|
||||
|
||||
string label = sug.Text;
|
||||
if (!string.IsNullOrEmpty(sug.Tooltip))
|
||||
label += " " + sug.Tooltip;
|
||||
|
||||
var tb = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Padding = new Thickness(1, 0),
|
||||
Foreground = Brushes.White,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
Background = i == _selectedSuggestionIndex
|
||||
? new SolidColorBrush(Color.FromRgb(0, 90, 160))
|
||||
: Brushes.Transparent,
|
||||
};
|
||||
|
||||
var row = new Border
|
||||
{
|
||||
Child = tb,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
|
||||
row.PointerPressed += (_, _) =>
|
||||
{
|
||||
_selectedSuggestionIndex = index;
|
||||
ApplySuggestionInPlace(index);
|
||||
_tabCycling = true;
|
||||
};
|
||||
row.PointerEntered += (_, _) =>
|
||||
{
|
||||
if (_selectedSuggestionIndex != index)
|
||||
{
|
||||
_selectedSuggestionIndex = index;
|
||||
UpdateSuggestionHighlight();
|
||||
}
|
||||
};
|
||||
|
||||
_suggestionPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
if (_suggestions.Length > MaxVisibleSuggestions)
|
||||
{
|
||||
string scrollHint = $"[{_suggestionViewTop + 1}-{viewBottom}/{_suggestions.Length}]";
|
||||
var hintTb = new TextBlock
|
||||
{
|
||||
Text = scrollHint,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(120, 120, 120)),
|
||||
Padding = new Thickness(1, 0),
|
||||
TextAlignment = TextAlignment.Right,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
};
|
||||
_suggestionPanel.Children.Add(hintTb);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSuggestionHighlight()
|
||||
{
|
||||
int visibleCount = Math.Min(_suggestions.Length, MaxVisibleSuggestions);
|
||||
for (int i = 0; i < visibleCount && i < _suggestionPanel.Children.Count; i++)
|
||||
{
|
||||
if (_suggestionPanel.Children[i] is Border border && border.Child is TextBlock tb)
|
||||
{
|
||||
int dataIndex = _suggestionViewTop + i;
|
||||
tb.Background = dataIndex == _selectedSuggestionIndex
|
||||
? new SolidColorBrush(Color.FromRgb(0, 90, 160))
|
||||
: Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveSuggestionSelection(int direction)
|
||||
{
|
||||
if (_suggestions.Length == 0) return;
|
||||
|
||||
_selectedSuggestionIndex += direction;
|
||||
if (_selectedSuggestionIndex < 0)
|
||||
_selectedSuggestionIndex = _suggestions.Length - 1;
|
||||
else if (_selectedSuggestionIndex >= _suggestions.Length)
|
||||
_selectedSuggestionIndex = 0;
|
||||
|
||||
int visibleCount = Math.Min(_suggestions.Length, MaxVisibleSuggestions);
|
||||
if (_selectedSuggestionIndex < _suggestionViewTop)
|
||||
{
|
||||
_suggestionViewTop = _selectedSuggestionIndex;
|
||||
RebuildSuggestionItems();
|
||||
}
|
||||
else if (_selectedSuggestionIndex >= _suggestionViewTop + visibleCount)
|
||||
{
|
||||
_suggestionViewTop = _selectedSuggestionIndex - visibleCount + 1;
|
||||
RebuildSuggestionItems();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateSuggestionHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSuggestionWheelChanged(object? sender, PointerWheelEventArgs e)
|
||||
{
|
||||
if (!SuggestionsVisible) return;
|
||||
|
||||
int direction = e.Delta.Y > 0 ? -1 : 1;
|
||||
ScrollSuggestionViewport(direction);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ScrollSuggestionViewport(int direction)
|
||||
{
|
||||
if (_suggestions.Length <= MaxVisibleSuggestions) return;
|
||||
|
||||
int newTop = _suggestionViewTop + direction;
|
||||
int maxTop = _suggestions.Length - MaxVisibleSuggestions;
|
||||
newTop = Math.Clamp(newTop, 0, maxTop);
|
||||
|
||||
if (newTop == _suggestionViewTop) return;
|
||||
_suggestionViewTop = newTop;
|
||||
|
||||
int viewBottom = _suggestionViewTop + MaxVisibleSuggestions;
|
||||
if (_selectedSuggestionIndex < _suggestionViewTop)
|
||||
_selectedSuggestionIndex = _suggestionViewTop;
|
||||
else if (_selectedSuggestionIndex >= viewBottom)
|
||||
_selectedSuggestionIndex = viewBottom - 1;
|
||||
|
||||
RebuildSuggestionItems();
|
||||
}
|
||||
|
||||
private void ApplySuggestionText(int index)
|
||||
{
|
||||
if (index < 0 || index >= _suggestions.Length) return;
|
||||
|
||||
string text = _commandInput.Text ?? "";
|
||||
string selected = _suggestions[index].Text;
|
||||
|
||||
int start = Math.Min(_suggestionRange.Start, text.Length);
|
||||
int end = Math.Min(_suggestionRange.End, text.Length);
|
||||
|
||||
string before = text[..start];
|
||||
string after = text[end..];
|
||||
string newText = before + selected + after;
|
||||
|
||||
_commandInput.Text = newText;
|
||||
_commandInput.CaretIndex = before.Length + selected.Length;
|
||||
|
||||
_suggestionRange = (start, start + selected.Length);
|
||||
}
|
||||
|
||||
private void ApplySuggestionInPlace(int index)
|
||||
{
|
||||
if (index < 0 || index >= _suggestions.Length) return;
|
||||
|
||||
_acceptingSuggestion = true;
|
||||
try
|
||||
{
|
||||
ApplySuggestionText(index);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_acceptingSuggestion = false;
|
||||
}
|
||||
UpdateSuggestionHighlight();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ctrl+C
|
||||
|
||||
internal void HandleCtrlC()
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
long elapsed = now - _lastCtrlCTicks;
|
||||
|
||||
if (_lastCtrlCTicks > 0 && elapsed < CtrlCDoublePressMsec)
|
||||
{
|
||||
HideNotification();
|
||||
TuiConsoleBackend.Instance?.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
_lastCtrlCTicks = now;
|
||||
|
||||
string inputText = _commandInput.Text?.Trim() ?? "";
|
||||
if (inputText.Length > 0)
|
||||
{
|
||||
_commandInput.Text = string.Empty;
|
||||
ShowNotification(Translations.tui_ctrlc_input_cleared, CtrlCDoublePressMsec);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowNotification(Translations.tui_ctrlc_quit_hint, CtrlCDoublePressMsec);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotification(string message, int autoHideMs)
|
||||
{
|
||||
_notificationText.Text = message;
|
||||
_notificationBorder.IsVisible = true;
|
||||
|
||||
var timer = new Avalonia.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(autoHideMs),
|
||||
};
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
timer.Stop();
|
||||
HideNotification();
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private void HideNotification()
|
||||
{
|
||||
_notificationBorder.IsVisible = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scrolling
|
||||
|
||||
private void ScrollLog(int delta)
|
||||
{
|
||||
var sv = _logScrollViewer;
|
||||
var newY = sv.Offset.Y + delta;
|
||||
newY = Math.Max(0, Math.Min(newY, sv.Extent.Height - sv.Viewport.Height));
|
||||
sv.Offset = new Vector(0, newY);
|
||||
|
||||
_autoScroll = newY >= sv.Extent.Height - sv.Viewport.Height - 2;
|
||||
}
|
||||
|
||||
private void OnLogScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (_programmaticScroll) return;
|
||||
|
||||
var sv = _logScrollViewer;
|
||||
_autoScroll = sv.Offset.Y >= sv.Extent.Height - sv.Viewport.Height - 2;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Status Bar (Health / Food)
|
||||
|
||||
private void StartStatusBarTimer()
|
||||
{
|
||||
var timer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromSeconds(1),
|
||||
};
|
||||
timer.Tick += (_, _) => UpdateStatusBar();
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private void UpdateStatusBar()
|
||||
{
|
||||
if (McClient.Instance is not McClient client)
|
||||
{
|
||||
_statusBar.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int gamemode = client.GetGamemode();
|
||||
if (gamemode != 0 && gamemode != 2)
|
||||
{
|
||||
_statusBar.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
float health = client.GetHealth();
|
||||
int food = client.GetSaturation();
|
||||
|
||||
int heartsFilled = (int)Math.Ceiling(health / 20f * 10);
|
||||
heartsFilled = Math.Clamp(heartsFilled, 0, 10);
|
||||
int foodFilled = (int)Math.Ceiling(food / 20f * 10);
|
||||
foodFilled = Math.Clamp(foodFilled, 0, 10);
|
||||
|
||||
_statusBar.Inlines?.Clear();
|
||||
_statusBar.Inlines ??= new Avalonia.Controls.Documents.InlineCollection();
|
||||
|
||||
var healthText = BuildBarText(heartsFilled, 10, "\u2764\ufe0f", " \u2661 ");
|
||||
var foodText = BuildBarText(foodFilled, 10, "\ud83c\udf56", " \u25cb ");
|
||||
|
||||
_statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(healthText)
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(255, 85, 85)),
|
||||
});
|
||||
_statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run($" {health:F1} ")
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(255, 150, 150)),
|
||||
});
|
||||
_statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run(foodText)
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(200, 160, 80)),
|
||||
});
|
||||
_statusBar.Inlines.Add(new Avalonia.Controls.Documents.Run($" {food}")
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(220, 190, 100)),
|
||||
});
|
||||
|
||||
_statusBar.IsVisible = true;
|
||||
}
|
||||
|
||||
private static string BuildBarText(int filled, int total, string filledChar, string emptyChar)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
for (int i = 0; i < filled; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(' ');
|
||||
sb.Append(filledChar);
|
||||
}
|
||||
for (int i = filled; i < total; i++)
|
||||
{
|
||||
sb.Append(emptyChar);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overlay
|
||||
|
||||
public void ShowOverlay(Control content, Action? onClose = null)
|
||||
{
|
||||
if (_overlayContent != null)
|
||||
HideOverlay();
|
||||
|
||||
_overlayContent = content;
|
||||
_overlayCloseCallback = onClose;
|
||||
_mainContent.IsVisible = false;
|
||||
|
||||
_rootPanel.Children.Add(_overlayContent);
|
||||
}
|
||||
|
||||
public void HideOverlay()
|
||||
{
|
||||
if (_overlayContent == null) return;
|
||||
|
||||
_rootPanel.Children.Remove(_overlayContent);
|
||||
|
||||
_overlayContent = null;
|
||||
_mainContent.IsVisible = true;
|
||||
|
||||
var cb = _overlayCloseCallback;
|
||||
_overlayCloseCallback = null;
|
||||
cb?.Invoke();
|
||||
|
||||
_commandInput.Focus();
|
||||
}
|
||||
|
||||
public bool HasOverlay => _overlayContent != null;
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape && _overlayContent != null)
|
||||
{
|
||||
HideOverlay();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
_commandInput.Focus();
|
||||
}, DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
MinecraftClient/Tui/McColorParser.cs
Normal file
114
MinecraftClient/Tui/McColorParser.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses Minecraft § color codes and produces Avalonia Inlines for rich text display.
|
||||
/// </summary>
|
||||
public static class McColorParser
|
||||
{
|
||||
private static readonly Dictionary<char, IBrush> ColorMap = new()
|
||||
{
|
||||
{ '0', new SolidColorBrush(Color.FromRgb(0, 0, 0)) },
|
||||
{ '1', new SolidColorBrush(Color.FromRgb(0, 0, 170)) },
|
||||
{ '2', new SolidColorBrush(Color.FromRgb(0, 170, 0)) },
|
||||
{ '3', new SolidColorBrush(Color.FromRgb(0, 170, 170)) },
|
||||
{ '4', new SolidColorBrush(Color.FromRgb(170, 0, 0)) },
|
||||
{ '5', new SolidColorBrush(Color.FromRgb(170, 0, 170)) },
|
||||
{ '6', new SolidColorBrush(Color.FromRgb(255, 170, 0)) },
|
||||
{ '7', new SolidColorBrush(Color.FromRgb(170, 170, 170)) },
|
||||
{ '8', new SolidColorBrush(Color.FromRgb(85, 85, 85)) },
|
||||
{ '9', new SolidColorBrush(Color.FromRgb(85, 85, 255)) },
|
||||
{ 'a', new SolidColorBrush(Color.FromRgb(85, 255, 85)) },
|
||||
{ 'b', new SolidColorBrush(Color.FromRgb(85, 255, 255)) },
|
||||
{ 'c', new SolidColorBrush(Color.FromRgb(255, 85, 85)) },
|
||||
{ 'd', new SolidColorBrush(Color.FromRgb(255, 85, 255)) },
|
||||
{ 'e', new SolidColorBrush(Color.FromRgb(255, 255, 85)) },
|
||||
{ 'f', Brushes.White },
|
||||
};
|
||||
|
||||
public static TextBlock CreateColoredTextBlock(string text, TextWrapping wrapping = TextWrapping.Wrap)
|
||||
{
|
||||
var tb = new TextBlock
|
||||
{
|
||||
TextWrapping = wrapping,
|
||||
Padding = new Avalonia.Thickness(0),
|
||||
Margin = new Avalonia.Thickness(0),
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(text) || !text.Contains('§'))
|
||||
{
|
||||
tb.Text = text ?? "";
|
||||
tb.Foreground = Brushes.White;
|
||||
return tb;
|
||||
}
|
||||
|
||||
IBrush currentColor = Brushes.White;
|
||||
bool bold = false;
|
||||
bool italic = false;
|
||||
int start = 0;
|
||||
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
if (text[i] == '§' && i + 1 < text.Length)
|
||||
{
|
||||
if (i > start)
|
||||
AddRun(tb, text[start..i], currentColor, bold, italic);
|
||||
|
||||
char code = char.ToLower(text[i + 1]);
|
||||
|
||||
if (ColorMap.TryGetValue(code, out var brush))
|
||||
{
|
||||
currentColor = brush;
|
||||
bold = false;
|
||||
italic = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case 'l': bold = true; break;
|
||||
case 'o': italic = true; break;
|
||||
case 'r':
|
||||
currentColor = Brushes.White;
|
||||
bold = false;
|
||||
italic = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (start < text.Length)
|
||||
AddRun(tb, text[start..], currentColor, bold, italic);
|
||||
|
||||
if (tb.Inlines?.Count == 0)
|
||||
{
|
||||
tb.Text = "";
|
||||
tb.Foreground = Brushes.White;
|
||||
}
|
||||
|
||||
return tb;
|
||||
}
|
||||
|
||||
private static void AddRun(TextBlock tb, string text, IBrush color, bool bold, bool italic)
|
||||
{
|
||||
if (text.Length == 0) return;
|
||||
|
||||
tb.Inlines ??= new InlineCollection();
|
||||
tb.Inlines.Add(new Run(text)
|
||||
{
|
||||
Foreground = color,
|
||||
FontWeight = bold ? FontWeight.Bold : FontWeight.Normal,
|
||||
FontStyle = italic ? FontStyle.Italic : FontStyle.Normal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
35
MinecraftClient/Tui/MccTuiApp.cs
Normal file
35
MinecraftClient/Tui/MccTuiApp.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media;
|
||||
using Consolonia.Themes;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class MccTuiApp : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
Styles.Add(new ModernTheme());
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var view = new MainTuiView();
|
||||
TuiConsoleBackend.Instance?.SetView(view);
|
||||
|
||||
desktop.MainWindow = new Window
|
||||
{
|
||||
Content = view,
|
||||
Title = "Minecraft Console Client",
|
||||
Background = Brushes.Black,
|
||||
Padding = new Thickness(0),
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
157
MinecraftClient/Tui/SlotViewModel.cs
Normal file
157
MinecraftClient/Tui/SlotViewModel.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class SlotViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private bool _isSelected;
|
||||
private bool _isHovered;
|
||||
|
||||
public int SlotId { get; }
|
||||
public string ItemDisplayText { get; private set; }
|
||||
public string CountDisplay { get; private set; }
|
||||
public string FullInfo { get; private set; }
|
||||
public string ItemTypeName { get; private set; }
|
||||
public bool IsEmpty { get; private set; }
|
||||
public bool IsHotbar { get; }
|
||||
public int HotbarIndex { get; }
|
||||
public ItemType ItemType { get; private set; }
|
||||
public int ItemCount { get; private set; }
|
||||
public Item? RawItem { get; private set; }
|
||||
public int NameMaxWidth { get; set; } = 9;
|
||||
public int NameMaxLines { get; set; } = 1;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set { _isSelected = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsHovered
|
||||
{
|
||||
get => _isHovered;
|
||||
set { _isHovered = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public SlotViewModel(int slotId, bool isHotbar = false, int hotbarIndex = -1)
|
||||
{
|
||||
SlotId = slotId;
|
||||
IsHotbar = isHotbar;
|
||||
HotbarIndex = hotbarIndex;
|
||||
ItemDisplayText = "";
|
||||
CountDisplay = "";
|
||||
FullInfo = "";
|
||||
ItemTypeName = "";
|
||||
IsEmpty = true;
|
||||
ItemType = ItemType.Air;
|
||||
ItemCount = 0;
|
||||
}
|
||||
|
||||
public void Update(Item? item)
|
||||
{
|
||||
RawItem = item;
|
||||
if (item == null || item.IsEmpty)
|
||||
{
|
||||
ItemDisplayText = "";
|
||||
CountDisplay = "";
|
||||
FullInfo = "";
|
||||
ItemTypeName = "";
|
||||
IsEmpty = true;
|
||||
ItemType = ItemType.Air;
|
||||
ItemCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemType = item.Type;
|
||||
ItemCount = item.Count;
|
||||
string typeName = item.GetTypeString();
|
||||
ItemTypeName = typeName;
|
||||
ItemDisplayText = FormatMultiLine(typeName, NameMaxWidth, NameMaxLines);
|
||||
CountDisplay = item.Count > 1 ? $"x{item.Count}" : "";
|
||||
FullInfo = item.ToFullString();
|
||||
IsEmpty = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(ItemDisplayText));
|
||||
OnPropertyChanged(nameof(CountDisplay));
|
||||
OnPropertyChanged(nameof(FullInfo));
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
OnPropertyChanged(nameof(ItemType));
|
||||
OnPropertyChanged(nameof(ItemTypeName));
|
||||
OnPropertyChanged(nameof(ItemCount));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format item name into multi-line display text that fits within
|
||||
/// maxWidth columns and maxLines lines. Breaks at word boundaries.
|
||||
/// </summary>
|
||||
private static string FormatMultiLine(string name, int maxWidth, int maxLines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return "";
|
||||
|
||||
int colonIdx = name.LastIndexOf(':');
|
||||
if (colonIdx >= 0 && colonIdx < name.Length - 1)
|
||||
name = name[(colonIdx + 1)..];
|
||||
|
||||
name = name.Replace("_", " ").Trim();
|
||||
name = InsertCamelCaseSpaces(name);
|
||||
|
||||
if (maxLines <= 1 || name.Length <= maxWidth)
|
||||
return name.Length <= maxWidth ? name : name[..maxWidth];
|
||||
|
||||
var lines = new System.Collections.Generic.List<string>();
|
||||
string remaining = name;
|
||||
|
||||
for (int line = 0; line < maxLines && remaining.Length > 0; line++)
|
||||
{
|
||||
if (remaining.Length <= maxWidth)
|
||||
{
|
||||
lines.Add(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
int breakAt = -1;
|
||||
for (int i = maxWidth; i >= 1; i--)
|
||||
{
|
||||
if (remaining[i] == ' ')
|
||||
{
|
||||
breakAt = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (breakAt < 0)
|
||||
breakAt = maxWidth;
|
||||
|
||||
lines.Add(remaining[..breakAt].TrimEnd());
|
||||
remaining = remaining[breakAt..].TrimStart();
|
||||
}
|
||||
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
private static string InsertCamelCaseSpaces(string s)
|
||||
{
|
||||
if (s.Length < 2) return s;
|
||||
var sb = new System.Text.StringBuilder(s.Length + 4);
|
||||
sb.Append(s[0]);
|
||||
for (int i = 1; i < s.Length; i++)
|
||||
{
|
||||
if (char.IsUpper(s[i]) && char.IsLower(s[i - 1]))
|
||||
sb.Append(' ');
|
||||
sb.Append(s[i]);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
306
MinecraftClient/Tui/TuiConsoleBackend.cs
Normal file
306
MinecraftClient/Tui/TuiConsoleBackend.cs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
using Consolonia;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
/// <summary>
|
||||
/// Console backend that uses Avalonia/Consolonia for a full-screen TUI.
|
||||
/// Avalonia Dispatcher runs on the main thread; MCC logic runs on background threads.
|
||||
/// </summary>
|
||||
public class TuiConsoleBackend : IConsoleBackend
|
||||
{
|
||||
public event EventHandler<string>? MessageReceived;
|
||||
public event EventHandler<ConsoleInputBuffer>? OnInputChange;
|
||||
|
||||
private MainTuiView? _view;
|
||||
private volatile bool _readThreadActive;
|
||||
|
||||
public bool DisplayUserInput { get; set; } = true;
|
||||
|
||||
internal static TuiConsoleBackend? Instance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the Avalonia app and starts the main UI loop.
|
||||
/// This blocks the calling thread until the TUI exits.
|
||||
/// Before blocking, it starts MCC's remaining initialization on a background thread.
|
||||
/// </summary>
|
||||
public void RunTuiMainLoop(string[] args)
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => RestoreTerminalState();
|
||||
|
||||
System.Console.CancelKeyPress += (_, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
var view = _view;
|
||||
if (view != null)
|
||||
Dispatcher.UIThread.Post(() => view.HandleCtrlC());
|
||||
};
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
ContinueMccStartup(args);
|
||||
})
|
||||
{ Name = "MCC-Main", IsBackground = true }.Start();
|
||||
|
||||
AppBuilder builder = AppBuilder.Configure<MccTuiApp>()
|
||||
.UseConsolonia()
|
||||
.UseAutoDetectedConsole()
|
||||
.LogToException();
|
||||
|
||||
try
|
||||
{
|
||||
builder.StartWithConsoleLifetime(Array.Empty<string>());
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreTerminalState();
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile bool _terminalRestored;
|
||||
|
||||
private static void RestoreTerminalState()
|
||||
{
|
||||
if (_terminalRestored) return;
|
||||
_terminalRestored = true;
|
||||
|
||||
try
|
||||
{
|
||||
System.Console.Write("\x1b[?1000l"); // disable X11 mouse
|
||||
System.Console.Write("\x1b[?1001l"); // disable highlight mouse
|
||||
System.Console.Write("\x1b[?1002l"); // disable button-event mouse
|
||||
System.Console.Write("\x1b[?1003l"); // disable any-event mouse
|
||||
System.Console.Write("\x1b[?1004l"); // disable focus events
|
||||
System.Console.Write("\x1b[?1005l"); // disable UTF-8 mouse encoding
|
||||
System.Console.Write("\x1b[?1006l"); // disable SGR mouse encoding
|
||||
System.Console.Write("\x1b[?1015l"); // disable urxvt mouse encoding
|
||||
System.Console.Write("\x1b[?1049l"); // leave alternate screen
|
||||
System.Console.Write("\x1b[?25h"); // show cursor
|
||||
System.Console.Write("\x1b[?7h"); // re-enable line wrap
|
||||
System.Console.Write("\x1b[0m"); // reset attributes
|
||||
System.Console.Write("\x1b[2J"); // clear entire screen
|
||||
System.Console.Write("\x1b[H"); // cursor to home
|
||||
System.Console.Out.Flush();
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var proc = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "stty",
|
||||
Arguments = "sane",
|
||||
UseShellExecute = false,
|
||||
});
|
||||
proc?.WaitForExit(500);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static void ContinueMccStartup(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Program.ContinueAfterTuiInit(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted($"§c[MCC] Fatal: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetView(MainTuiView view)
|
||||
{
|
||||
_view = view;
|
||||
}
|
||||
|
||||
internal MainTuiView? GetView() => _view;
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public void WriteLine(string text)
|
||||
{
|
||||
var view = _view;
|
||||
if (view == null)
|
||||
{
|
||||
System.Console.WriteLine(text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
view.AppendLogLine(text);
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => view.AppendLogLine(text));
|
||||
}
|
||||
|
||||
public void WriteLineFormatted(string text)
|
||||
{
|
||||
var view = _view;
|
||||
if (view == null)
|
||||
{
|
||||
System.Console.WriteLine(Scripting.ChatBot.GetVerbatim(text));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
view.AppendFormattedLogLine(text);
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => view.AppendFormattedLogLine(text));
|
||||
}
|
||||
|
||||
public void BeginReadThread()
|
||||
{
|
||||
_readThreadActive = true;
|
||||
}
|
||||
|
||||
public void StopReadThread()
|
||||
{
|
||||
_readThreadActive = false;
|
||||
DismissOverlay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close any open overlay (e.g. inventory) so the user can interact
|
||||
/// with the main console again. Safe to call from any thread.
|
||||
/// </summary>
|
||||
internal void DismissOverlay()
|
||||
{
|
||||
var view = _view;
|
||||
if (view == null) return;
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
view.HideOverlay();
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => view.HideOverlay());
|
||||
}
|
||||
}
|
||||
|
||||
public string RequestImmediateInput()
|
||||
{
|
||||
if (_shutdownRequested)
|
||||
{
|
||||
Thread.Sleep(Timeout.Infinite);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var mre = new ManualResetEventSlim(false);
|
||||
string? result = null;
|
||||
|
||||
void Handler(object? sender, string e)
|
||||
{
|
||||
result = e;
|
||||
mre.Set();
|
||||
}
|
||||
|
||||
MessageReceived += Handler;
|
||||
mre.Wait();
|
||||
MessageReceived -= Handler;
|
||||
|
||||
return result ?? string.Empty;
|
||||
}
|
||||
|
||||
public string? ReadPassword()
|
||||
{
|
||||
return RequestImmediateInput();
|
||||
}
|
||||
|
||||
public void ClearInputBuffer()
|
||||
{
|
||||
if (_view == null) return;
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
_view.ClearInput();
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => _view?.ClearInput());
|
||||
}
|
||||
|
||||
public void SetInputVisible(bool visible)
|
||||
{
|
||||
}
|
||||
|
||||
public void SetBackreadBufferLimit(int limit)
|
||||
{
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_shutdownRequested = true;
|
||||
RestoreTerminalState();
|
||||
|
||||
var lifetime = Application.Current?.ApplicationLifetime
|
||||
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime;
|
||||
|
||||
if (lifetime != null)
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
lifetime.Shutdown();
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => lifetime.Shutdown());
|
||||
}
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
Environment.Exit(0);
|
||||
}) { Name = "TUI-Exit-Guard", IsBackground = true }.Start();
|
||||
}
|
||||
|
||||
private volatile bool _shutdownRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Called from the TUI view when user presses Enter in the command input.
|
||||
/// Always fires MessageReceived so that both the normal read-thread path
|
||||
/// and RequestImmediateInput (used by offline prompt) receive the input.
|
||||
/// </summary>
|
||||
internal void OnCommandSubmitted(string command)
|
||||
{
|
||||
MessageReceived?.Invoke(this, command);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from the TUI view when user types in the command input.
|
||||
/// </summary>
|
||||
internal void OnInputChanged(string text, int cursorPos)
|
||||
{
|
||||
OnInputChange?.Invoke(this, new ConsoleInputBuffer(text, cursorPos));
|
||||
}
|
||||
|
||||
internal void UpdateSuggestions(CommandSuggestion[] suggestions, (int Start, int End) range)
|
||||
{
|
||||
var view = _view;
|
||||
if (view == null) return;
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
view.UpdateSuggestions(suggestions, range);
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => view.UpdateSuggestions(suggestions, range));
|
||||
}
|
||||
|
||||
internal void ClearSuggestions()
|
||||
{
|
||||
var view = _view;
|
||||
if (view == null) return;
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
view.ClearSuggestions();
|
||||
else
|
||||
Dispatcher.UIThread.Post(() => view.ClearSuggestions());
|
||||
}
|
||||
}
|
||||
}
|
||||
194
tools/mcc-debug.sh
Normal file
194
tools/mcc-debug.sh
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
#!/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"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: tools/mcc-debug.sh [options]
|
||||
|
||||
One-step build, server start, and MCC launch for debugging.
|
||||
|
||||
Options:
|
||||
-v, --version VER Server directory name (default: 1.21.11-Vanilla)
|
||||
-m, --mode MODE Console mode: classic or tui (default: classic)
|
||||
-p, --port PORT Server port (default: 25565)
|
||||
--no-build Skip dotnet build
|
||||
--debug-on Enable debug messages from the start
|
||||
--file-input Use FileInput mode (classic only; enables mcc-cmd)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
tools/mcc-debug.sh # Classic mode, default server
|
||||
tools/mcc-debug.sh -m tui # TUI mode
|
||||
tools/mcc-debug.sh -v 1.21.11-Vanilla --debug-on
|
||||
tools/mcc-debug.sh --file-input # FileInput for script-driven testing
|
||||
EOF
|
||||
}
|
||||
|
||||
VERSION="1.21.11-Vanilla"
|
||||
MODE="classic"
|
||||
PORT="25565"
|
||||
DO_BUILD=true
|
||||
DEBUG_ON=false
|
||||
FILE_INPUT=false
|
||||
|
||||
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 ;;
|
||||
--no-build) DO_BUILD=false; shift ;;
|
||||
--debug-on) DEBUG_ON=true; shift ;;
|
||||
--file-input) FILE_INPUT=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
TEST_ROOT="${TMPDIR:-/tmp}/mcc-debug"
|
||||
CFG="$TEST_ROOT/MinecraftClient.debug.ini"
|
||||
MCC_LOG="$TEST_ROOT/mcc-debug.log"
|
||||
INPUT_FILE="$REPO_ROOT/mcc_input.txt"
|
||||
SESSION_NAME="mc-${VERSION//\./_}"
|
||||
|
||||
mkdir -p "$TEST_ROOT"
|
||||
|
||||
echo "=== MCC Debug Session ==="
|
||||
echo " Server: $VERSION (port $PORT)"
|
||||
echo " Mode: $MODE"
|
||||
echo " Config: $CFG"
|
||||
echo " Log: $MCC_LOG"
|
||||
echo ""
|
||||
|
||||
# --- Build ---
|
||||
if $DO_BUILD; then
|
||||
echo "[1/4] Building MCC..."
|
||||
dotnet build "$REPO_ROOT/MinecraftClient.sln" -c Release -v quiet --nologo
|
||||
echo " Build OK"
|
||||
else
|
||||
echo "[1/4] Build skipped (--no-build)"
|
||||
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"
|
||||
|
||||
if [[ "$MODE" == "tui" ]]; then
|
||||
sed -i 's/ConsoleMode = "classic"/ConsoleMode = "tui"/' "$CFG"
|
||||
fi
|
||||
|
||||
if $DEBUG_ON; then
|
||||
sed -i 's/DebugMessages = false/DebugMessages = true/' "$CFG"
|
||||
fi
|
||||
|
||||
echo " Config ready"
|
||||
|
||||
# --- Start server ---
|
||||
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
|
||||
mc-start "$VERSION" >/dev/null
|
||||
|
||||
echo -n " Waiting for server..."
|
||||
for i in $(seq 1 60); do
|
||||
if mc-log "$VERSION" 250 2>/dev/null | grep -Fq "Done ("; then
|
||||
echo " ready (${i}s)"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
if [[ $i -eq 60 ]]; then
|
||||
echo " TIMEOUT"
|
||||
echo "Server failed to start. Check: tmux attach -t $SESSION_NAME"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Launch MCC ---
|
||||
echo "[4/4] Launching MCC in $MODE mode..."
|
||||
: > "$INPUT_FILE"
|
||||
rm -f "$MCC_LOG"
|
||||
|
||||
MCC_ARGS=("$CFG" "CursorBot" "-" "localhost:$PORT")
|
||||
|
||||
if [[ "$MODE" == "tui" ]]; then
|
||||
# TUI mode: needs a real tty — no pipes or redirects allowed
|
||||
tmux kill-session -t mcc-debug 2>/dev/null || true
|
||||
tmux new-session -d -s mcc-debug -x 160 -y 50 \
|
||||
"cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600"
|
||||
echo ""
|
||||
echo " TUI mode started in tmux session 'mcc-debug'"
|
||||
echo " (TUI mode uses a real terminal; log file is not available, use MCC's /debug command)"
|
||||
echo ""
|
||||
echo " Attach: tmux attach -t mcc-debug"
|
||||
echo " Detach: Ctrl+B, D"
|
||||
echo " Kill MCC: tmux kill-session -t mcc-debug"
|
||||
echo ""
|
||||
elif $FILE_INPUT; then
|
||||
# FileInput mode: run in background, drive via mcc_input.txt
|
||||
(
|
||||
cd "$REPO_ROOT"
|
||||
MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release --no-build -- "${MCC_ARGS[@]}" > "$MCC_LOG" 2>&1
|
||||
) &
|
||||
MCC_PID=$!
|
||||
echo " MCC PID: $MCC_PID"
|
||||
echo ""
|
||||
|
||||
sleep 5
|
||||
if kill -0 "$MCC_PID" 2>/dev/null; then
|
||||
if grep -Fq "Server was successfully joined" "$MCC_LOG" 2>/dev/null; then
|
||||
echo " MCC connected successfully!"
|
||||
else
|
||||
echo " MCC started (check $MCC_LOG for status)"
|
||||
fi
|
||||
else
|
||||
echo " MCC exited unexpectedly. Check $MCC_LOG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Send commands: echo 'debug state' >> $INPUT_FILE"
|
||||
echo " Tail log: tail -f $MCC_LOG"
|
||||
echo " Stop MCC: echo 'quit' >> $INPUT_FILE"
|
||||
echo " Stop server: mc-stop $VERSION"
|
||||
echo ""
|
||||
else
|
||||
# Interactive classic mode: run in tmux (no pipe — ConsoleInteractive also needs tty)
|
||||
tmux kill-session -t mcc-debug 2>/dev/null || true
|
||||
tmux new-session -d -s mcc-debug -x 160 -y 50 \
|
||||
"cd '$REPO_ROOT' && dotnet run --project MinecraftClient -c Release --no-build -- ${MCC_ARGS[*]}; echo '=== MCC EXITED ==='; sleep 600"
|
||||
echo ""
|
||||
echo " Classic mode started in tmux session 'mcc-debug'"
|
||||
echo ""
|
||||
echo " Attach: tmux attach -t mcc-debug"
|
||||
echo " Detach: Ctrl+B, D"
|
||||
echo " Kill MCC: tmux kill-session -t mcc-debug"
|
||||
echo " Note: Use MCC's built-in /debug command or enable LogToFile for log output"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Quick commands:"
|
||||
echo " mc-rcon 'op CursorBot' # Give operator"
|
||||
echo " mc-rcon 'gamemode creative' # Creative mode"
|
||||
echo " mc-stop $VERSION # Stop server"
|
||||
|
|
@ -38,9 +38,24 @@ mcc-run() {
|
|||
cd "$MCC_REPO" && MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${port}" "$@" 2>&1
|
||||
}
|
||||
mcc-cmd() { echo "$1" >> "$MCC_REPO/mcc_input.txt"; }
|
||||
mcc-kill() { pkill -f "MinecraftClient" 2>/dev/null && echo "MCC killed" || echo "No MCC process found"; }
|
||||
mcc-kill() { pkill -f "MinecraftClient" 2>/dev/null && echo "MCC killed" || echo "No MCC process found"; tmux kill-session -t mcc-debug 2>/dev/null || true; }
|
||||
mcc-reload() {
|
||||
mcc-kill
|
||||
sleep 1
|
||||
mcc-build && mcc-run
|
||||
}
|
||||
|
||||
# --- TUI Mode ---
|
||||
mcc-tui() {
|
||||
local port="${1:-25565}"
|
||||
shift || true
|
||||
tmux new-session -d -s mcc-debug -x 160 -y 50 \
|
||||
"cd '$MCC_REPO' && dotnet run --project MinecraftClient -c Release -- CursorBot - localhost:${port} $* 2>&1; echo '=== MCC EXITED ==='; sleep 600"
|
||||
echo "TUI mode launched in tmux session 'mcc-debug'"
|
||||
echo "Attach: tmux attach -t mcc-debug"
|
||||
}
|
||||
|
||||
# --- Debug helpers ---
|
||||
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; }
|
||||
|
|
|
|||
54
tools/mcc-log-tail.sh
Normal file
54
tools/mcc-log-tail.sh
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#!/usr/bin/env bash
|
||||
# Tail MCC and/or server logs side-by-side or individually.
|
||||
# Usage:
|
||||
# tools/mcc-log-tail.sh # tail MCC log only
|
||||
# tools/mcc-log-tail.sh --server VER # tail both MCC and server logs
|
||||
# tools/mcc-log-tail.sh --server-only VER # tail server log only
|
||||
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"
|
||||
|
||||
MCC_LOG="${TMPDIR:-/tmp}/mcc-debug/mcc-debug.log"
|
||||
SERVER_VER=""
|
||||
SERVER_ONLY=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--server) SERVER_VER="$2"; shift 2 ;;
|
||||
--server-only) SERVER_ONLY=true; SERVER_VER="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: tools/mcc-log-tail.sh [--server VER] [--server-only VER]"
|
||||
exit 0 ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if $SERVER_ONLY; then
|
||||
if [[ -z "$SERVER_VER" ]]; then
|
||||
echo "Specify server version with --server-only VER" >&2
|
||||
exit 1
|
||||
fi
|
||||
SERVER_LOG="$MCC_SERVERS/$SERVER_VER/logs/latest.log"
|
||||
echo "=== Tailing server log: $SERVER_LOG ==="
|
||||
exec tail -f "$SERVER_LOG"
|
||||
fi
|
||||
|
||||
if [[ -n "$SERVER_VER" ]]; then
|
||||
SERVER_LOG="$MCC_SERVERS/$SERVER_VER/logs/latest.log"
|
||||
echo "=== Tailing MCC + server logs ==="
|
||||
echo " MCC: $MCC_LOG"
|
||||
echo " Server: $SERVER_LOG"
|
||||
echo ""
|
||||
tail -f "$MCC_LOG" "$SERVER_LOG" 2>/dev/null
|
||||
else
|
||||
if [[ ! -f "$MCC_LOG" ]]; then
|
||||
echo "No MCC log found at $MCC_LOG"
|
||||
echo "Start MCC first with: tools/mcc-debug.sh --file-input"
|
||||
exit 1
|
||||
fi
|
||||
echo "=== Tailing MCC log: $MCC_LOG ==="
|
||||
exec tail -f "$MCC_LOG"
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue